first commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
out/
|
||||
*.step
|
||||
*.stl
|
||||
*.json
|
||||
*.FCStd
|
||||
*.FCBak
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Basic shape-first modeling with the functional API.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/01_basic_modeling.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
base = scad.make_box_rsolid(60.0, 36.0, 8.0, bottom_face_center=(0.0, 0.0, 0.0))
|
||||
hole = scad.make_cylinder_rsolid(5.0, 14.0, bottom_face_center=(0.0, 0.0, -3.0))
|
||||
slot = scad.make_box_rsolid(18.0, 8.0, 14.0, bottom_face_center=(14.0, 0.0, -3.0))
|
||||
part = scad.cut_rsolid(base, hole, slot)
|
||||
|
||||
boss = scad.make_cylinder_rsolid(8.0, 7.0, bottom_face_center=(-18.0, 0.0, 8.0))
|
||||
part = scad.union_rsolid(part, boss)
|
||||
part.auto_tag_faces("box")
|
||||
|
||||
print("volume", round(part.get_volume(), 3))
|
||||
print("faces", len(part.get_faces()))
|
||||
print("edges", len(part.get_edges()))
|
||||
|
||||
scad.export_step(part, str(OUT / "basic_modeling.step"))
|
||||
scad.export_stl(part, str(OUT / "basic_modeling.stl"))
|
||||
print("wrote", OUT / "basic_modeling.step")
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Record a replayable model graph, export model JSON, then replay it.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/02_graph_replay.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql as Q
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
body = scad.make_box_rsolid(40.0, 24.0, 10.0, bottom_face_center=(0.0, 0.0, 0.0))
|
||||
cutter = scad.make_cylinder_rsolid(4.0, 16.0, bottom_face_center=(0.0, 0.0, -3.0))
|
||||
drilled = scad.cut_rsolid(body, cutter)
|
||||
|
||||
# Use a serializable QL selector instead of relying on OCC edge iteration order.
|
||||
bottom_circle = (
|
||||
Q.edges()
|
||||
.where(Q.curve_type("circle"))
|
||||
.order_by(Q.center_axis("z"))
|
||||
.take(1)
|
||||
.exactly(1)
|
||||
)
|
||||
final = scad.chamfer_rsolid(drilled, bottom_circle, 0.6)
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
(OUT / "graph_replay.model.json").write_text(model_json, encoding="utf-8")
|
||||
|
||||
rebuilt = scad.replay_model_json(model_json)
|
||||
print("recorded_nodes", session.graph.node_count)
|
||||
print("replayed_outputs", len(rebuilt))
|
||||
print("replayed_type", type(rebuilt[0]).__name__ if rebuilt else "none")
|
||||
print("wrote", OUT / "graph_replay.model.json")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Expression parameters inside a replayable graph/model workflow.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/03_expressions.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
width = scad.var("width", 24.0, comment="plate width")
|
||||
height = scad.var("height", 12.0, comment="plate height")
|
||||
thickness = scad.var("thickness", 4.0, comment="plate thickness")
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
plate = scad.make_box_rsolid(width, height, thickness)
|
||||
rib = scad.make_box_rsolid(width / 4.0, height, thickness * 2.0)
|
||||
rib = scad.translate_shape(rib, (0.0, 0.0, 4.0))
|
||||
part = scad.union_rsolid(plate, rib)
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
payload = json.loads(model_json)
|
||||
(OUT / "expressions.model.json").write_text(model_json, encoding="utf-8")
|
||||
|
||||
print("expression_nodes", len(payload["expression_graph"]["nodes"]))
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("volume", round(part.get_volume(), 3))
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Profile operations: revolve, loft, and sweep.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/05_loft_sweep_revolve.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Revolve a closed profile into a small knob.
|
||||
profile = scad.make_polyline_rwire(
|
||||
[(0.0, 0.0, 0.0), (4.0, 0.0, 0.0), (3.0, 0.0, 8.0), (1.0, 0.0, 8.0)],
|
||||
closed=True,
|
||||
)
|
||||
knob = scad.revolve_rsolid(profile, axis=(0.0, 0.0, 1.0), angle=360.0)
|
||||
|
||||
# Loft between rectangular sections.
|
||||
a = scad.make_rectangle_rwire(8.0, 8.0, center=(16.0, 0.0, 0.0))
|
||||
b = scad.make_rectangle_rwire(4.0, 4.0, center=(16.0, 0.0, 8.0))
|
||||
loft = scad.loft_rsolid([a, b])
|
||||
|
||||
# Sweep a circular face along a polyline path.
|
||||
profile_face = scad.make_circle_rface((30.0, 0.0, 0.0), 1.0, normal=(1.0, 0.0, 0.0))
|
||||
path = scad.make_polyline_rwire([(30.0, 0.0, 0.0), (34.0, 0.0, 3.0), (38.0, 3.0, 6.0)])
|
||||
swept = scad.sweep_rsolid(profile_face, path)
|
||||
|
||||
scad.export_step([knob, loft, swept], str(OUT / "profile_operations.step"))
|
||||
print("knob", round(knob.get_volume(), 3))
|
||||
print("loft", round(loft.get_volume(), 3))
|
||||
print("swept", round(swept.get_volume(), 3))
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Tidy parametric gear-like model JSON example.
|
||||
|
||||
This example is intentionally lightweight enough for automated tests. It is not a
|
||||
full involute gear generator; it demonstrates the same release-critical behavior:
|
||||
expression parameters, derived numeric construction values, canonical graph
|
||||
export, and exactly one explicit `leaf_ids` output.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/06_parametric_gear_model.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def _involute_spur_profile_points(
|
||||
*,
|
||||
tooth_count: int,
|
||||
module: float,
|
||||
pressure_angle_deg: float = 20.0,
|
||||
backlash: float = 0.03,
|
||||
profile_points: int = 10,
|
||||
root_arc_points: int = 4,
|
||||
tip_arc_points: int = 4,
|
||||
) -> list[tuple[float, float, float]]:
|
||||
"""Sample a closed 2D involute spur gear outline in the XY plane."""
|
||||
|
||||
import math
|
||||
|
||||
if tooth_count < 8:
|
||||
raise ValueError("tooth_count must be >= 8")
|
||||
if module <= 0:
|
||||
raise ValueError("module must be > 0")
|
||||
|
||||
pressure_angle = math.radians(pressure_angle_deg)
|
||||
pitch_radius = 0.5 * module * tooth_count
|
||||
tip_radius = pitch_radius + module
|
||||
root_radius = pitch_radius - 1.25 * module
|
||||
base_radius = pitch_radius * math.cos(pressure_angle)
|
||||
if root_radius <= 0:
|
||||
raise ValueError("invalid gear dimensions: root radius <= 0")
|
||||
|
||||
half_tooth_angle = (math.pi / (2.0 * tooth_count)) - (
|
||||
backlash / (2.0 * pitch_radius)
|
||||
)
|
||||
inv_pitch = math.tan(pressure_angle) - pressure_angle
|
||||
r_start = max(root_radius, base_radius)
|
||||
|
||||
def flank_angle_at_radius(radius: float) -> float:
|
||||
ratio = min(1.0, max(0.0, base_radius / radius))
|
||||
phi = math.acos(ratio)
|
||||
inv_r = math.tan(phi) - phi
|
||||
return half_tooth_angle + inv_pitch - inv_r
|
||||
|
||||
radii = [
|
||||
r_start + (tip_radius - r_start) * i / (profile_points - 1)
|
||||
for i in range(profile_points)
|
||||
]
|
||||
|
||||
flank_pos: list[tuple[float, float]] = []
|
||||
flank_neg: list[tuple[float, float]] = []
|
||||
for radius in radii:
|
||||
beta = flank_angle_at_radius(radius)
|
||||
x = radius * math.cos(beta)
|
||||
y = radius * math.sin(beta)
|
||||
flank_pos.append((x, y))
|
||||
flank_neg.append((x, -y))
|
||||
|
||||
start_angle = math.atan2(flank_pos[0][1], flank_pos[0][0])
|
||||
tip_angle_neg = math.atan2(flank_neg[-1][1], flank_neg[-1][0])
|
||||
tip_angle_pos = math.atan2(flank_pos[-1][1], flank_pos[-1][0])
|
||||
|
||||
tip_arc = [
|
||||
(
|
||||
tip_radius
|
||||
* math.cos(
|
||||
tip_angle_neg + (tip_angle_pos - tip_angle_neg) * i / tip_arc_points
|
||||
),
|
||||
tip_radius
|
||||
* math.sin(
|
||||
tip_angle_neg + (tip_angle_pos - tip_angle_neg) * i / tip_arc_points
|
||||
),
|
||||
)
|
||||
for i in range(1, tip_arc_points)
|
||||
]
|
||||
|
||||
tooth_local: list[tuple[float, float]] = []
|
||||
tooth_local.append((root_radius * math.cos(-start_angle), root_radius * math.sin(-start_angle)))
|
||||
tooth_local.extend(flank_neg)
|
||||
tooth_local.extend(tip_arc)
|
||||
tooth_local.extend(reversed(flank_pos))
|
||||
tooth_local.append((root_radius * math.cos(start_angle), root_radius * math.sin(start_angle)))
|
||||
|
||||
def rotate_xy(point: tuple[float, float], angle: float) -> tuple[float, float]:
|
||||
c = math.cos(angle)
|
||||
s = math.sin(angle)
|
||||
return (point[0] * c - point[1] * s, point[0] * s + point[1] * c)
|
||||
|
||||
tooth_pitch_angle = 2.0 * math.pi / tooth_count
|
||||
outline: list[tuple[float, float]] = []
|
||||
for k in range(tooth_count):
|
||||
center_angle = k * tooth_pitch_angle
|
||||
tooth_world = [rotate_xy(point, center_angle) for point in tooth_local]
|
||||
outline.extend(tooth_world if not outline else tooth_world[1:])
|
||||
|
||||
a0 = center_angle + start_angle
|
||||
a1 = center_angle + tooth_pitch_angle - start_angle
|
||||
for i in range(1, root_arc_points):
|
||||
angle = a0 + (a1 - a0) * i / root_arc_points
|
||||
outline.append((root_radius * math.cos(angle), root_radius * math.sin(angle)))
|
||||
|
||||
cleaned: list[tuple[float, float]] = []
|
||||
for point in outline:
|
||||
if not cleaned:
|
||||
cleaned.append(point)
|
||||
continue
|
||||
if math.hypot(point[0] - cleaned[-1][0], point[1] - cleaned[-1][1]) > 1e-7:
|
||||
cleaned.append(point)
|
||||
|
||||
return [(x, y, 0.0) for x, y in cleaned]
|
||||
|
||||
|
||||
def build_model(output_dir: Path) -> dict:
|
||||
"""Build a small replayable involute spur gear and write model JSON.
|
||||
|
||||
Args:
|
||||
output_dir: Directory that receives `parametric_gear.model.json`.
|
||||
|
||||
Returns:
|
||||
The parsed exported model payload.
|
||||
"""
|
||||
|
||||
tooth_count_value = 14
|
||||
module_value = 1.4
|
||||
thickness = scad.var("thickness", 4.0)
|
||||
bore_radius = scad.var("bore_radius", 2.2)
|
||||
|
||||
# Keep derived construction facts as numerics; this example verifies they do
|
||||
# not become top-level model variables such as `pitch_radius`.
|
||||
profile_points = _involute_spur_profile_points(
|
||||
tooth_count=tooth_count_value,
|
||||
module=module_value,
|
||||
)
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
profile = scad.make_polyline_rwire(profile_points, closed=True)
|
||||
gear = scad.extrude_rsolid(profile, (0.0, 0.0, 1.0), thickness)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
bore_radius,
|
||||
thickness + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
)
|
||||
gear = scad.cut_rsolid(gear, bore)
|
||||
# Keep the final output as a single explicit leaf node.
|
||||
gear = scad.translate_shape(gear, (0.0, 0.0, 0.0))
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
payload = json.loads(model_json)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(output_dir / "parametric_gear.model.json").write_text(model_json, encoding="utf-8")
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=Path("examples/out/parametric_gear"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
payload = build_model(args.output_dir)
|
||||
var_names = [
|
||||
node.get("name")
|
||||
for node in payload["expression_graph"]["nodes"]
|
||||
if node.get("kind") == "var"
|
||||
]
|
||||
|
||||
print("leaf_count", len(payload["leaf_ids"]))
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("vars", ",".join(str(name) for name in var_names))
|
||||
print("wrote", args.output_dir / "parametric_gear.model.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Show how source code maps to the serializable operation tree.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/07_serialization_operation_tree.py
|
||||
|
||||
This example intentionally keeps the geometry simple. Its main purpose is to
|
||||
show that user-facing calls such as `make_box_rsolid()` and
|
||||
`helical_sweep_rsolid()` are lowered into the canonical, replayable operation
|
||||
nodes stored in `model.json`.
|
||||
|
||||
Generated files:
|
||||
examples/out/serialization_operation_tree.model.json
|
||||
examples/out/serialization_operation_tree.summary.md
|
||||
examples/out/serialization_operation_tree.step
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql as Q
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
MODEL_JSON_PATH = OUT / "serialization_operation_tree.model.json"
|
||||
SUMMARY_PATH = OUT / "serialization_operation_tree.summary.md"
|
||||
STEP_PATH = OUT / "serialization_operation_tree.step"
|
||||
|
||||
|
||||
def source_step(name: str):
|
||||
"""Print a readable marker while building the recorded model."""
|
||||
print(f"SOURCE STEP: {name}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expression parameters: model JSON stores numeric snapshots in node.params and
|
||||
# expression references in node.param_exprs / expression_graph.
|
||||
# ---------------------------------------------------------------------------
|
||||
plate_w = scad.var("plate_w", 36.0, comment="main plate width")
|
||||
plate_h = scad.var("plate_h", 18.0, comment="main plate height")
|
||||
plate_t = scad.var("plate_t", 3.0, comment="main plate thickness")
|
||||
hole_r = scad.var("hole_r", 2.2, comment="through-hole radius")
|
||||
rib_t = scad.var("rib_t", 1.6, comment="rib thickness")
|
||||
fillet_r = scad.var("fillet_r", 0.45, comment="small edge fillet radius")
|
||||
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
# Basic construction and primitive lowering:
|
||||
# make_box_rsolid -> rectangle face -> four line edges -> wire -> face -> extrude
|
||||
source_step("01 make_box_rsolid(expr dimensions) -> lowered profile + extrude")
|
||||
plate = scad.make_box_rsolid(plate_w, plate_h, plate_t)
|
||||
plate = scad.apply_tag(plate, "demo.main_plate")
|
||||
|
||||
# make_cylinder_rsolid is also serializable via lowering:
|
||||
# circle edge -> wire -> face -> extrude
|
||||
source_step("02 make_cylinder_rsolid(expr radius) -> lowered circle face + extrude")
|
||||
hole = scad.make_cylinder_rsolid(
|
||||
hole_r,
|
||||
plate_t + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
)
|
||||
drilled_plate = scad.cut_rsolid(plate, hole)
|
||||
|
||||
# Core wire/profile API: point, line, circle, arc, spline, helix, wire construction,
|
||||
# face construction. These are kept small and placed away from the plate so
|
||||
# they are easy to inspect in the graph without making the shape complicated.
|
||||
source_step("03 make_point_rvertex")
|
||||
marker_point = scad.make_point_rvertex(-18.0, -9.0, 6.0)
|
||||
|
||||
source_step("04 explicit edges + make_wire_from_edges_rwire + make_face_from_wire_rface")
|
||||
e1 = scad.make_line_redge((-8.0, 0.0, plate_t), (-6.0, 0.0, plate_t))
|
||||
e2 = scad.make_three_point_arc_redge(
|
||||
(-6.0, 0.0, plate_t), (-5.0, 1.0, plate_t), (-4.0, 0.0, plate_t)
|
||||
)
|
||||
e3 = scad.make_angle_arc_redge(
|
||||
(-3.0, 0.0, plate_t), 1.0, 3.14159, 0.0, normal=(0.0, 0.0, 1.0)
|
||||
)
|
||||
spline_fit = scad.fit_cubic_bspline_control_points(
|
||||
[(-2.0, 0.0, plate_t), (-1.0, 0.8, plate_t), (0.0, 0.0, plate_t)],
|
||||
tolerance=0.01,
|
||||
)
|
||||
e4 = scad.make_spline_redge(
|
||||
control_points=spline_fit.control_points,
|
||||
knots=spline_fit.unique_knots,
|
||||
multiplicities=spline_fit.multiplicities,
|
||||
)
|
||||
# The four edges above are intentionally separate leaf examples. A valid
|
||||
# `make_wire_from_edges_rwire` example follows with a closed triangle.
|
||||
|
||||
# A closed profile built explicitly from lines, then converted to a face.
|
||||
tri_a = scad.make_line_redge((8.0, -2.0, plate_t), (11.0, -2.0, plate_t))
|
||||
tri_b = scad.make_line_redge((11.0, -2.0, plate_t), (9.5, 1.0, plate_t))
|
||||
tri_c = scad.make_line_redge((9.5, 1.0, plate_t), (8.0, -2.0, plate_t))
|
||||
triangle_wire = scad.make_wire_from_edges_rwire([tri_a, tri_b, tri_c])
|
||||
triangle_face = scad.make_face_from_wire_rface(triangle_wire)
|
||||
triangle_boss = scad.extrude_rsolid(triangle_face, (0.0, 0.0, 1.0), rib_t)
|
||||
|
||||
# Convenience wire/face builders are included too; inside GraphSession they
|
||||
# lower to the same canonical low-level edge/wire/face operations.
|
||||
source_step("05 convenience wires/faces -> lowered canonical edge/wire/face nodes")
|
||||
rectangle_wire = scad.make_rectangle_rwire(4.0, 2.0, center=(-13.0, 0.0, plate_t))
|
||||
rectangle_face = scad.make_rectangle_rface(4.0, 2.0, center=(-13.0, 4.0, plate_t))
|
||||
circle_wire = scad.make_circle_rwire((13.0, 4.0, plate_t), 1.0)
|
||||
circle_face = scad.make_circle_rface((13.0, 0.0, plate_t), 1.0)
|
||||
segment_wire = scad.make_segment_rwire((-13.0, -4.0, plate_t), (-9.0, -4.0, plate_t))
|
||||
polyline_wire = scad.make_polyline_rwire(
|
||||
[(-3.0, -5.0, plate_t), (-1.0, -4.0, plate_t), (1.0, -5.0, plate_t)]
|
||||
)
|
||||
arc_wire = scad.make_three_point_arc_rwire(
|
||||
(3.0, -5.0, plate_t), (4.0, -4.0, plate_t), (5.0, -5.0, plate_t)
|
||||
)
|
||||
angle_arc_wire = scad.make_angle_arc_rwire((7.0, -5.0, plate_t), 1.0, 0.0, 1.57)
|
||||
wire_spline_fit = scad.fit_cubic_bspline_control_points(
|
||||
[(9.0, -5.0, plate_t), (10.0, -4.0, plate_t), (11.0, -5.0, plate_t)],
|
||||
tolerance=0.01,
|
||||
)
|
||||
spline_wire = scad.make_spline_rwire(
|
||||
control_points=wire_spline_fit.control_points,
|
||||
knots=wire_spline_fit.unique_knots,
|
||||
multiplicities=wire_spline_fit.multiplicities,
|
||||
)
|
||||
|
||||
# Basic solid constructors that lower to replayable core operations.
|
||||
source_step("06 make_sphere_rsolid, make_cone_rsolid")
|
||||
sphere = scad.make_sphere_rsolid(1.0, center=(-7.0, 6.0, plate_t + 1.0))
|
||||
cone = scad.make_cone_rsolid(
|
||||
1.2,
|
||||
2.0,
|
||||
top_radius=0.4,
|
||||
bottom_face_center=(-3.0, 6.0, plate_t),
|
||||
)
|
||||
# Feature operations.
|
||||
source_step("07 revolve_rsolid, loft_rsolid, sweep_rsolid")
|
||||
revolve_profile = scad.make_polyline_rwire(
|
||||
[(0.5, 0.0, 0.0), (1.2, 0.0, 0.0), (1.0, 0.0, 1.6), (0.5, 0.0, 1.6)],
|
||||
closed=True,
|
||||
)
|
||||
revolved_pin = scad.revolve_rsolid(
|
||||
revolve_profile,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
angle=360.0,
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
revolved_pin = scad.translate_shape(revolved_pin, (4.0, 6.0, plate_t))
|
||||
|
||||
loft_a = scad.make_rectangle_rwire(1.8, 1.2, center=(8.0, 6.0, plate_t))
|
||||
loft_b = scad.make_rectangle_rwire(1.0, 0.8, center=(8.0, 6.0, plate_t + 2.0))
|
||||
lofted_post = scad.loft_rsolid([loft_a, loft_b], ruled=True)
|
||||
|
||||
sweep_profile = scad.make_circle_rface((12.0, 6.0, plate_t), 0.35, normal=(1.0, 0.0, 0.0))
|
||||
sweep_path = scad.make_polyline_rwire(
|
||||
[(12.0, 6.0, plate_t), (14.0, 6.0, plate_t + 1.0), (15.5, 7.0, plate_t + 1.5)]
|
||||
)
|
||||
swept_pipe = scad.sweep_rsolid(sweep_profile, sweep_path, is_frenet=False)
|
||||
|
||||
# Composite operation: helical_sweep_rsolid is serialized as helix + face + sweep,
|
||||
# not as a dedicated `helical_sweep` graph node.
|
||||
source_step("08 helical_sweep_rsolid macro -> make_helix_redge + wire + face + sweep")
|
||||
thread_profile = scad.make_rectangle_rwire(0.25, 0.18, center=(0.0, 0.0, 0.0))
|
||||
helical_thread = scad.helical_sweep_rsolid(
|
||||
thread_profile,
|
||||
pitch=0.7,
|
||||
height=2.2,
|
||||
radius=0.9,
|
||||
center=(13.0, -6.0, plate_t),
|
||||
)
|
||||
|
||||
# Transforms and patterns. Pattern helpers serialize as explicit translate /
|
||||
# rotate nodes instead of `linear_pattern` / `radial_pattern` macro nodes.
|
||||
source_step("09 translate_shape, rotate_shape, mirror_shape")
|
||||
rib = scad.make_box_rsolid(rib_t, plate_h * 0.55, plate_t * 1.4)
|
||||
rib = scad.translate_shape(rib, (-plate_w / 4.0, 0.0, plate_t))
|
||||
rib = scad.rotate_shape(rib, 0.0) # zero-angle shortcut, intentionally not recorded
|
||||
rib_copy = scad.mirror_shape(rib, plane_origin=(0.0, 0.0, 0.0), plane_normal=(1.0, 0.0, 0.0))
|
||||
|
||||
source_step("10 linear_pattern_rsolidlist and radial_pattern_rsolidlist macro lowering")
|
||||
lug_seed = scad.make_box_rsolid(1.2, 1.2, 1.0, bottom_face_center=(-12.0, -7.0, plate_t))
|
||||
linear_lugs = scad.linear_pattern_rsolidlist(lug_seed, (1.0, 0.0, 0.0), count=3, spacing=3.0)
|
||||
spoke_seed = scad.make_box_rsolid(0.8, 2.0, 0.8, bottom_face_center=(0.0, 5.2, plate_t))
|
||||
radial_spokes = scad.radial_pattern_rsolidlist(
|
||||
spoke_seed,
|
||||
center=(0.0, 0.0, plate_t),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
count=4,
|
||||
total_rotation_angle=360.0,
|
||||
)
|
||||
|
||||
# Boolean operations. Boolean union must produce one connected solid, so
|
||||
# this tiny demo uses overlapping boxes instead of trying to merge every
|
||||
# separate showcase solid above.
|
||||
source_step("11 union_rsolid, intersect_rsolid, cut_rsolid")
|
||||
union_a = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(-4.0, -7.0, 0.0))
|
||||
union_b = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(-2.5, -7.0, 0.0))
|
||||
union_demo = scad.union_rsolid(union_a, union_b)
|
||||
|
||||
overlap_a = scad.make_box_rsolid(2.0, 2.0, 2.0, bottom_face_center=(12.0, -2.0, plate_t))
|
||||
overlap_b = scad.make_box_rsolid(2.0, 2.0, 2.0, bottom_face_center=(13.0, -2.0, plate_t))
|
||||
intersection_demo = scad.intersect_rsolid(overlap_a, overlap_b)
|
||||
|
||||
# Detail operations use QL selectors so the graph contains stable, serializable
|
||||
# selection hints rather than Python object identity from source code.
|
||||
source_step("12 fillet_rsolid, chamfer_rsolid, shell_rsolid with serializable selectors")
|
||||
vertical_edges = Q.edges().where(Q.curve_type("line")).take(4)
|
||||
final = scad.fillet_rsolid(union_demo, vertical_edges, fillet_r)
|
||||
|
||||
chamfer_box = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(2.0, -7.0, 0.0))
|
||||
top_outer_edges = Q.edges().order_by(Q.center_axis("z"), desc=True).take(4)
|
||||
chamfer_demo = scad.chamfer_rsolid(chamfer_box, top_outer_edges, 0.15)
|
||||
|
||||
# Keep shell separate so the demo includes shell without making the main part
|
||||
# fragile. It remains a replayable leaf in model.json.
|
||||
shell_box = scad.make_box_rsolid(4.0, 3.0, 2.0, bottom_face_center=(18.0, -7.0, 0.0))
|
||||
top_face = Q.faces().order_by(Q.center_axis("z"), desc=True).take(1).exactly(1)
|
||||
shell_demo = scad.shell_rsolid(shell_box, top_face, 0.25)
|
||||
|
||||
# Export the canonical model JSON and inspect how the graph maps back to source.
|
||||
model_json = scad.export_model_json(session)
|
||||
payload = json.loads(model_json)
|
||||
MODEL_JSON_PATH.write_text(model_json, encoding="utf-8")
|
||||
|
||||
# Replay from model JSON to prove that the stored operation tree is sufficient.
|
||||
rebuilt = scad.replay_model_json(model_json)
|
||||
scad.export_step(rebuilt, str(STEP_PATH))
|
||||
|
||||
ops = [node["op"] for node in payload["graph"]["nodes"]]
|
||||
op_counts = Counter(ops)
|
||||
expr_nodes = payload["expression_graph"]["nodes"]
|
||||
nodes_with_exprs = [
|
||||
node for node in payload["graph"]["nodes"] if node.get("param_exprs")
|
||||
]
|
||||
|
||||
# Build a compact source-to-graph explanation. This file is easier to read than
|
||||
# the full JSON and is meant to be opened side by side with this Python source.
|
||||
summary = dedent(
|
||||
f"""
|
||||
# Serialization Operation Tree Example
|
||||
|
||||
Source file: `examples/07_serialization_operation_tree.py`
|
||||
|
||||
Generated model JSON: `{MODEL_JSON_PATH}`
|
||||
Generated STEP replay output: `{STEP_PATH}`
|
||||
|
||||
## What to compare
|
||||
|
||||
1. Read the `SOURCE STEP` comments / print output in the Python source.
|
||||
2. Open the JSON and inspect `graph.nodes[*].op`, `params`, `param_exprs`, and `inputs`.
|
||||
3. Notice that convenience API calls are lowered to canonical replayable operations.
|
||||
|
||||
## Basic counts
|
||||
|
||||
- graph nodes: `{len(payload['graph']['nodes'])}`
|
||||
- graph edges: `{len(payload['graph']['edges'])}`
|
||||
- leaf ids: `{len(payload['leaf_ids'])}` -> `{payload['leaf_ids']}`
|
||||
- expression graph nodes: `{len(expr_nodes)}`
|
||||
- operation nodes with `param_exprs`: `{len(nodes_with_exprs)}`
|
||||
- replayed outputs: `{len(rebuilt)}`
|
||||
|
||||
## Canonical operation set observed
|
||||
|
||||
"""
|
||||
).lstrip()
|
||||
|
||||
for op, count in sorted(op_counts.items()):
|
||||
summary += f"- `{op}`: {count}\n"
|
||||
|
||||
summary += dedent(
|
||||
"""
|
||||
|
||||
## Important source-code to graph mappings
|
||||
|
||||
- `make_box_rsolid(...)` does **not** appear as `make_box` in model JSON.
|
||||
It lowers to `make_line_redge` + `make_wire_from_edges_rwire` +
|
||||
`make_face_from_wire_rface` + `make_extrude_rsolid`.
|
||||
- `make_cylinder_rsolid(...)` lowers to a circle face plus `make_extrude_rsolid`.
|
||||
- `make_sphere_rsolid(...)` and `make_cone_rsolid(...)` lower to revolve chains.
|
||||
- `make_rectangle_rwire`, `make_circle_rwire`, `make_polyline_rwire`, and
|
||||
single-arc/spline/helix wire helpers lower to edge + wire operations.
|
||||
- `linear_pattern_rsolidlist(...)` lowers to explicit `make_translate_rshape`
|
||||
nodes.
|
||||
- `radial_pattern_rsolidlist(...)` lowers to explicit `make_rotate_rshape`
|
||||
nodes.
|
||||
- `helical_sweep_rsolid(...)` lowers to helix + face + `make_sweep_rsolid`;
|
||||
there is no `helical_sweep` node.
|
||||
- Expression values are snapshotted into `params`; the symbolic links live in
|
||||
`param_exprs` and the top-level `expression_graph`.
|
||||
|
||||
## Nodes that reference expressions
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
for node in nodes_with_exprs[:40]:
|
||||
summary += f"- `{node['node_id']}` `{node['op']}` param_exprs={json.dumps(node['param_exprs'], sort_keys=True)}\n"
|
||||
|
||||
if len(nodes_with_exprs) > 40:
|
||||
summary += f"- ... {len(nodes_with_exprs) - 40} more expression-backed nodes\n"
|
||||
|
||||
SUMMARY_PATH.write_text(summary, encoding="utf-8")
|
||||
|
||||
print("wrote", MODEL_JSON_PATH)
|
||||
print("wrote", SUMMARY_PATH)
|
||||
print("wrote", STEP_PATH)
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("expression_nodes", len(expr_nodes))
|
||||
print("leaf_ids", payload["leaf_ids"])
|
||||
print("replayed_outputs", len(rebuilt))
|
||||
print("observed_ops", ", ".join(sorted(op_counts)))
|
||||
@@ -0,0 +1,397 @@
|
||||
"""Constrained sketch-first modeling with isomorphic SimpleCADAPI calls.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/08_constrained_sketch.py
|
||||
|
||||
Generated files:
|
||||
examples/out/constrained_sketch.model.json
|
||||
examples/out/constrained_sketch.step
|
||||
examples/out/constrained_sketch.fcstd
|
||||
|
||||
When the intent is a sketch/profile, use the sketch APIs. Concrete geometry
|
||||
APIs remain for paths, pure geometry, and lowering targets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
MODEL_JSON_PATH = OUT / "constrained_sketch.model.json"
|
||||
STEP_PATH = OUT / "constrained_sketch.step"
|
||||
FCSTD_PATH = OUT / "constrained_sketch.fcstd"
|
||||
FREECAD_CMD = Path("/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd")
|
||||
|
||||
|
||||
def _solve_and_report(name: str, sketch: scad.Sketch) -> None:
|
||||
result = scad.inspect_sketch_rsketchresult(
|
||||
sketch,
|
||||
require_fully_constrained=True,
|
||||
)
|
||||
points = sorted(
|
||||
(point_id, round(point[0], 3), round(point[1], 3))
|
||||
for point_id, point in result.solved_points.items()
|
||||
)
|
||||
scalars = sorted(
|
||||
(key, round(value, 3)) for key, value in result.solved_scalars.items()
|
||||
)
|
||||
print(
|
||||
f"{name}_sketch",
|
||||
result.status,
|
||||
"dof",
|
||||
result.dof,
|
||||
"residual",
|
||||
f"{result.residual_norm:.2e}",
|
||||
"points",
|
||||
points[:4],
|
||||
"scalars",
|
||||
scalars[:2],
|
||||
)
|
||||
|
||||
|
||||
def _promote_face(name: str, sketch: scad.Sketch):
|
||||
_solve_and_report(name, sketch)
|
||||
return scad.make_face_from_sketch_rface(
|
||||
sketch,
|
||||
require_fully_constrained=True,
|
||||
)
|
||||
|
||||
|
||||
def make_rect_profile(name, x0, y0, width, height):
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
|
||||
sketch = scad.add_point_rsketch(sketch, "p0", x0, y0)
|
||||
sketch = scad.add_point_rsketch(sketch, "p1", x0 + width, y0)
|
||||
sketch = scad.add_point_rsketch(sketch, "p2", x0 + width, y0 + height)
|
||||
sketch = scad.add_point_rsketch(sketch, "p3", x0, y0 + height)
|
||||
|
||||
sketch = scad.add_line_rsketch(sketch, "bottom", "p0", "p1")
|
||||
sketch = scad.add_line_rsketch(sketch, "right", "p1", "p2")
|
||||
sketch = scad.add_line_rsketch(sketch, "top", "p2", "p3")
|
||||
sketch = scad.add_line_rsketch(sketch, "left", "p3", "p0")
|
||||
|
||||
sketch = scad.constrain_horizontal_rsketch(sketch, "bottom")
|
||||
sketch = scad.constrain_vertical_rsketch(sketch, "right")
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "bottom", "top")
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "left", "right")
|
||||
sketch = scad.constrain_perpendicular_rsketch(sketch, "bottom", "right")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "bottom", "top")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "left", "right")
|
||||
sketch = scad.constrain_distance_rsketch(sketch, "p0", "p1", width)
|
||||
sketch = scad.constrain_distance_rsketch(sketch, "p0", "p3", height)
|
||||
sketch = scad.constrain_fix_rsketch(sketch, "p0")
|
||||
return _promote_face(name, sketch)
|
||||
|
||||
|
||||
def make_circle_profile(name, center_x, center_y, radius, circle_id):
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
sketch = scad.add_point_rsketch(sketch, "center", center_x, center_y)
|
||||
sketch = scad.add_circle_rsketch(sketch, circle_id, "center", radius)
|
||||
sketch = scad.constrain_fix_rsketch(sketch, "center")
|
||||
sketch = scad.constrain_radius_rsketch(sketch, circle_id, radius)
|
||||
return _promote_face(name, sketch)
|
||||
|
||||
|
||||
def make_guided_diamond_profile(name, center_x, center_y, width, height, guide_gap):
|
||||
half_w = width / 2.0
|
||||
half_h = height / 2.0
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
|
||||
sketch = scad.add_point_rsketch(sketch, "center", center_x, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "left", center_x - half_w, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "top", center_x, center_y + half_h)
|
||||
sketch = scad.add_point_rsketch(sketch, "right", center_x + half_w, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "bottom", center_x, center_y - half_h)
|
||||
|
||||
sketch = scad.add_point_rsketch(sketch, "guide_upper_start", center_x - half_w, center_y + guide_gap)
|
||||
sketch = scad.add_point_rsketch(sketch, "guide_upper_end", center_x, center_y + half_h + guide_gap)
|
||||
sketch = scad.add_point_rsketch(sketch, "guide_lower_start", center_x + half_w, center_y - guide_gap)
|
||||
sketch = scad.add_point_rsketch(sketch, "guide_lower_end", center_x, center_y - half_h - guide_gap)
|
||||
|
||||
sketch = scad.add_line_rsketch(sketch, "bottom_left", "left", "bottom")
|
||||
sketch = scad.add_line_rsketch(sketch, "right_bottom", "bottom", "right")
|
||||
sketch = scad.add_line_rsketch(sketch, "top_right", "right", "top")
|
||||
sketch = scad.add_line_rsketch(sketch, "left_top", "top", "left")
|
||||
sketch = scad.add_line_rsketch(sketch, "guide_upper", "guide_upper_start", "guide_upper_end", construction=True)
|
||||
sketch = scad.add_line_rsketch(sketch, "guide_lower", "guide_lower_start", "guide_lower_end", construction=True)
|
||||
|
||||
sketch = scad.constrain_fix_rsketch(sketch, "center")
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "left", "center", half_w)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "left", "center", 0.0)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "right", half_w)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "center", "right", 0.0)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "top", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "center", "top", half_h)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "bottom", "center", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "bottom", "center", half_h)
|
||||
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "left_top", "right_bottom")
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "top_right", "bottom_left")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "left_top", "top_right")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "top_right", "right_bottom")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "right_bottom", "bottom_left")
|
||||
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "left", "guide_upper_start", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "left", "guide_upper_start", guide_gap)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "top", "guide_upper_end", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "top", "guide_upper_end", guide_gap)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "guide_lower_start", "right", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "guide_lower_start", "right", guide_gap)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "guide_lower_end", "bottom", 0.0)
|
||||
sketch = scad.constrain_distance_y_rsketch(sketch, "guide_lower_end", "bottom", guide_gap)
|
||||
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "guide_upper", "guide_lower")
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "guide_upper", "right_bottom")
|
||||
sketch = scad.constrain_parallel_rsketch(sketch, "guide_lower", "left_top")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "guide_upper", "right_bottom")
|
||||
sketch = scad.constrain_equal_length_rsketch(sketch, "guide_lower", "left_top")
|
||||
return _promote_face(name, sketch)
|
||||
|
||||
|
||||
def make_curve_guided_relief_profile(name, center_x, center_y, radius, guide_span):
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
|
||||
sketch = scad.add_point_rsketch(sketch, "center", center_x, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "rim", center_x + radius, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "clearance_center", center_x, center_y)
|
||||
sketch = scad.add_point_rsketch(sketch, "upper_left", center_x - guide_span, center_y + radius)
|
||||
sketch = scad.add_point_rsketch(sketch, "upper_right", center_x + guide_span, center_y + radius)
|
||||
sketch = scad.add_point_rsketch(sketch, "lower_left", center_x - guide_span, center_y - radius)
|
||||
sketch = scad.add_point_rsketch(sketch, "lower_right", center_x + guide_span, center_y - radius)
|
||||
|
||||
sketch = scad.add_circle_rsketch(sketch, "relief", "center", radius)
|
||||
sketch = scad.add_circle_rsketch(sketch, "clearance", "clearance_center", radius, construction=True)
|
||||
sketch = scad.add_line_rsketch(sketch, "radius_probe", "center", "rim", construction=True)
|
||||
sketch = scad.add_line_rsketch(sketch, "upper_rail", "upper_left", "upper_right", construction=True)
|
||||
sketch = scad.add_line_rsketch(sketch, "lower_rail", "lower_left", "lower_right", construction=True)
|
||||
|
||||
sketch = scad.constrain_fix_rsketch(sketch, "center")
|
||||
sketch = scad.constrain_radius_rsketch(sketch, "relief", radius)
|
||||
sketch = scad.constrain_point_on_rsketch(sketch, "rim", "relief")
|
||||
sketch = scad.constrain_horizontal_rsketch(sketch, "radius_probe")
|
||||
sketch = scad.constrain_length_rsketch(sketch, "radius_probe", radius)
|
||||
|
||||
sketch = scad.constrain_concentric_rsketch(sketch, "relief", "clearance")
|
||||
sketch = scad.constrain_equal_radius_rsketch(sketch, "relief", "clearance")
|
||||
sketch = scad.constrain_horizontal_rsketch(sketch, "upper_rail")
|
||||
sketch = scad.constrain_horizontal_rsketch(sketch, "lower_rail")
|
||||
sketch = scad.constrain_tangent_rsketch(sketch, "upper_rail", "relief")
|
||||
sketch = scad.constrain_tangent_rsketch(sketch, "lower_rail", "relief")
|
||||
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "upper_left", -guide_span)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "upper_right", guide_span)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "lower_left", -guide_span)
|
||||
sketch = scad.constrain_distance_x_rsketch(sketch, "center", "lower_right", guide_span)
|
||||
return _promote_face(name, sketch)
|
||||
|
||||
|
||||
plate_w = scad.var("plate_w", 96.0, comment="plate width")
|
||||
plate_h = scad.var("plate_h", 54.0, comment="plate height")
|
||||
plate_t = scad.var("plate_t", 6.0, comment="plate thickness")
|
||||
boss_r = scad.var("boss_r", 14.0, comment="raised center boss radius")
|
||||
boss_h = scad.var("boss_h", 5.0, comment="raised center boss height")
|
||||
bore_r = scad.var("bore_r", 5.0, comment="through bore radius")
|
||||
mount_r = scad.var("mount_r", 3.0, comment="mounting hole radius")
|
||||
margin_x = scad.var("mount_margin_x", 12.0, comment="mounting hole x margin")
|
||||
margin_y = scad.var("mount_margin_y", 9.0, comment="mounting hole y margin")
|
||||
slot_w = scad.var("slot_w", 34.0, comment="service slot width")
|
||||
slot_h = scad.var("slot_h", 8.0, comment="service slot height")
|
||||
slot_y = scad.var("slot_center_y", 16.0, comment="service slot center y")
|
||||
diamond_w = scad.var("guided_diamond_w", 14.0, comment="guided diamond pocket width")
|
||||
diamond_h = scad.var("guided_diamond_h", 8.0, comment="guided diamond pocket height")
|
||||
diamond_guide_gap = scad.var("guided_diamond_guide_gap", 5.0, comment="parallel guide rail offset")
|
||||
relief_r = scad.var("curve_relief_r", 4.0, comment="curve-guided relief radius")
|
||||
relief_guide_span = scad.var("curve_relief_guide_span", 9.0, comment="curve relief construction rail half span")
|
||||
|
||||
center_x = plate_w / 2.0
|
||||
center_y = plate_h / 2.0
|
||||
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
plate_profile = make_rect_profile("plate_outline", 0.0, 0.0, plate_w, plate_h)
|
||||
plate_profile = scad.apply_tag(plate_profile, "demo.profile.plate")
|
||||
plate = scad.extrude_rsolid(plate_profile, (0.0, 0.0, 1.0), plate_t)
|
||||
plate = scad.apply_tag(plate, "demo.body.base_plate")
|
||||
|
||||
boss_profile = make_circle_profile(
|
||||
"center_boss",
|
||||
center_x,
|
||||
center_y,
|
||||
boss_r,
|
||||
"boss_outer",
|
||||
)
|
||||
boss_overlap = 1.0
|
||||
boss = scad.extrude_rsolid(boss_profile, (0.0, 0.0, 1.0), boss_h + boss_overlap)
|
||||
boss = scad.translate_shape(boss, (0.0, 0.0, plate_t - boss_overlap))
|
||||
boss = scad.apply_tag(boss, "demo.body.raised_boss")
|
||||
|
||||
body = scad.union_rsolid(plate, boss, glue=False)
|
||||
|
||||
bore_profile = make_circle_profile(
|
||||
"center_bore",
|
||||
center_x,
|
||||
center_y,
|
||||
bore_r,
|
||||
"bore",
|
||||
)
|
||||
bore_cutter = scad.extrude_rsolid(
|
||||
bore_profile,
|
||||
(0.0, 0.0, 1.0),
|
||||
plate_t + boss_h + 2.0,
|
||||
)
|
||||
bore_cutter = scad.translate_shape(bore_cutter, (0.0, 0.0, -1.0))
|
||||
|
||||
slot_profile = make_rect_profile(
|
||||
"service_slot",
|
||||
center_x - slot_w / 2.0,
|
||||
slot_y - slot_h / 2.0,
|
||||
slot_w,
|
||||
slot_h,
|
||||
)
|
||||
slot_cutter = scad.extrude_rsolid(slot_profile, (0.0, 0.0, 1.0), plate_t + 2.0)
|
||||
slot_cutter = scad.translate_shape(slot_cutter, (0.0, 0.0, -1.0))
|
||||
|
||||
diamond_profile = make_guided_diamond_profile(
|
||||
"guided_diamond_pocket",
|
||||
plate_w - 24.0,
|
||||
plate_h - 18.0,
|
||||
diamond_w,
|
||||
diamond_h,
|
||||
diamond_guide_gap,
|
||||
)
|
||||
diamond_cutter = scad.extrude_rsolid(diamond_profile, (0.0, 0.0, 1.0), plate_t + 2.0)
|
||||
diamond_cutter = scad.translate_shape(diamond_cutter, (0.0, 0.0, -1.0))
|
||||
|
||||
curve_relief_profile = make_curve_guided_relief_profile(
|
||||
"curve_guided_relief",
|
||||
plate_w / 3.0,
|
||||
plate_h - 12.0,
|
||||
relief_r,
|
||||
relief_guide_span,
|
||||
)
|
||||
curve_relief_cutter = scad.extrude_rsolid(curve_relief_profile, (0.0, 0.0, 1.0), plate_t + 2.0)
|
||||
curve_relief_cutter = scad.translate_shape(curve_relief_cutter, (0.0, 0.0, -1.0))
|
||||
|
||||
mount_centers = [
|
||||
("mount_sw", margin_x, margin_y),
|
||||
("mount_se", plate_w - margin_x, margin_y),
|
||||
("mount_ne", plate_w - margin_x, plate_h - margin_y),
|
||||
("mount_nw", margin_x, plate_h - margin_y),
|
||||
]
|
||||
mount_cutters = []
|
||||
for name, x_pos, y_pos in mount_centers:
|
||||
mount_profile = make_circle_profile(name, x_pos, y_pos, mount_r, "mount_hole")
|
||||
mount_cutter = scad.extrude_rsolid(
|
||||
mount_profile,
|
||||
(0.0, 0.0, 1.0),
|
||||
plate_t + 2.0,
|
||||
)
|
||||
mount_cutters.append(scad.translate_shape(mount_cutter, (0.0, 0.0, -1.0)))
|
||||
|
||||
part = scad.cut_rsolid(
|
||||
body,
|
||||
bore_cutter,
|
||||
slot_cutter,
|
||||
diamond_cutter,
|
||||
curve_relief_cutter,
|
||||
mount_cutters,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
part = scad.apply_tag(part, "demo.constrained_sketch_bracket")
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
MODEL_JSON_PATH.write_text(model_json, encoding="utf-8")
|
||||
|
||||
rebuilt = scad.replay_model_json(model_json)
|
||||
scad.export_step(rebuilt, str(STEP_PATH))
|
||||
|
||||
freecad_cmd = str(FREECAD_CMD) if FREECAD_CMD.exists() else None
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
model_json,
|
||||
str(FCSTD_PATH),
|
||||
document_name="SimpleCADConstrainedSketchDemo",
|
||||
freecad_cmd=freecad_cmd,
|
||||
)
|
||||
|
||||
payload = json.loads(model_json)
|
||||
ops = [node["op"] for node in payload["graph"]["nodes"]]
|
||||
promotion_nodes = [
|
||||
node
|
||||
for node in payload["graph"]["nodes"]
|
||||
if node["op"] in {"make_face_from_sketch_rface", "make_wire_from_sketch_rwire"}
|
||||
]
|
||||
diamond_promotion = next(
|
||||
node
|
||||
for node in promotion_nodes
|
||||
if node["params"]["sketch"].get("name") == "guided_diamond_pocket"
|
||||
)
|
||||
diamond_constraints = diamond_promotion["params"]["sketch"].get("constraints", [])
|
||||
curve_promotion = next(
|
||||
node
|
||||
for node in promotion_nodes
|
||||
if node["params"]["sketch"].get("name") == "curve_guided_relief"
|
||||
)
|
||||
curve_constraints = curve_promotion["params"]["sketch"].get("constraints", [])
|
||||
sketch_entity_tags = sorted(
|
||||
tag
|
||||
for edge in scad.ql.select(plate_profile.get_edges()).where(
|
||||
scad.ql.tag("sketch_entity.*")
|
||||
).all()
|
||||
for tag in scad.list_tags(edge)
|
||||
if tag.startswith("sketch_entity.")
|
||||
)
|
||||
diamond_entity_tags = sorted(
|
||||
tag
|
||||
for edge in scad.ql.select(diamond_profile.get_edges()).where(
|
||||
scad.ql.tag("sketch_entity.*")
|
||||
).all()
|
||||
for tag in scad.list_tags(edge)
|
||||
if tag.startswith("sketch_entity.")
|
||||
)
|
||||
curve_entity_tags = sorted(
|
||||
tag
|
||||
for edge in scad.ql.select(curve_relief_profile.get_edges()).where(
|
||||
scad.ql.tag("sketch_entity.*")
|
||||
).all()
|
||||
for tag in scad.list_tags(edge)
|
||||
if tag.startswith("sketch_entity.")
|
||||
)
|
||||
|
||||
print("graph_nodes", len(ops))
|
||||
print("sketch_ops", sum(1 for op in ops if "sketch" in op))
|
||||
print("promotion_nodes", len(promotion_nodes))
|
||||
print(
|
||||
"promotion_solve_snapshots",
|
||||
sum(1 for node in promotion_nodes if "solve_snapshot" in node.get("params", {})),
|
||||
)
|
||||
print("contains_public_solve_node", "make_solve_sketch_rsketchresult" in ops)
|
||||
print("plate_sketch_entity_tags", sketch_entity_tags)
|
||||
print("diamond_sketch_entity_tags", diamond_entity_tags)
|
||||
print("diamond_constraint_count", len(diamond_constraints))
|
||||
print(
|
||||
"diamond_parallel_equal_constraints",
|
||||
sum(
|
||||
1
|
||||
for constraint in diamond_constraints
|
||||
if constraint.get("kind") in {"parallel", "equal_length"}
|
||||
),
|
||||
)
|
||||
print("curve_sketch_entity_tags", curve_entity_tags)
|
||||
print("curve_constraint_count", len(curve_constraints))
|
||||
print(
|
||||
"curve_tangent_equal_radius_constraints",
|
||||
sum(
|
||||
1
|
||||
for constraint in curve_constraints
|
||||
if constraint.get("kind") in {"tangent", "equal_radius", "concentric", "point_on"}
|
||||
),
|
||||
)
|
||||
print("volume", round(part.get_volume(), 3))
|
||||
print("wrote", MODEL_JSON_PATH)
|
||||
print("wrote", STEP_PATH)
|
||||
print("wrote", FCSTD_PATH)
|
||||
@@ -0,0 +1,96 @@
|
||||
"""NACA 0016 propeller blade with exact BSpline FreeCAD translation.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/09_naca0016_blade_freecad.py
|
||||
|
||||
Generated files:
|
||||
examples/out/naca0016_blade/naca0016_blade.model.json
|
||||
examples/out/naca0016_blade/naca0016_blade.step
|
||||
examples/out/naca0016_blade/naca0016_blade.fcstd
|
||||
|
||||
The NACA section generator starts from sampled airfoil points. The evolve helper
|
||||
fits those samples into exact cubic B-spline control data before calling
|
||||
`make_spline_rwire(...)`, so the exported model JSON and FreeCAD document contain
|
||||
exact B-spline payloads rather than sampled-point spline approximations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
DEFAULT_OUTPUT_DIR = Path("examples/out/naca0016_blade")
|
||||
DEFAULT_FREECAD_CMD = Path("/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd")
|
||||
|
||||
|
||||
def build_blade(output_dir: Path, *, freecad_cmd: Path | None = DEFAULT_FREECAD_CMD) -> dict:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_json_path = output_dir / "naca0016_blade.model.json"
|
||||
step_path = output_dir / "naca0016_blade.step"
|
||||
fcstd_path = output_dir / "naca0016_blade.fcstd"
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
blade = scad.make_naca_propeller_blade_rsolid(
|
||||
blade_length=4.0,
|
||||
root_chord=1.25,
|
||||
tip_chord=0.35,
|
||||
total_twist_angle=36.0,
|
||||
num_sections=6,
|
||||
)
|
||||
blade = scad.apply_tag(blade, "role.naca0016.blade")
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
payload = json.loads(model_json)
|
||||
model_json_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(blade, str(step_path))
|
||||
|
||||
bspline_nodes = [
|
||||
node for node in payload["graph"]["nodes"] if node.get("op") == "make_spline_redge"
|
||||
]
|
||||
loft_nodes = [node for node in payload["graph"]["nodes"] if node.get("op") == "make_loft_rsolid"]
|
||||
control_counts = [len(node["params"].get("control_points", [])) for node in bspline_nodes]
|
||||
knot_counts = [len(node["params"].get("knots", [])) for node in bspline_nodes]
|
||||
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("leaf_ids", payload["leaf_ids"])
|
||||
print("bspline_section_nodes", len(bspline_nodes))
|
||||
print("loft_nodes", len(loft_nodes))
|
||||
print("bspline_control_counts", control_counts[:6])
|
||||
print("bspline_knot_counts", knot_counts[:6])
|
||||
print("volume", round(blade.get_volume(), 6))
|
||||
print("wrote", model_json_path)
|
||||
print("wrote", step_path)
|
||||
|
||||
if freecad_cmd is not None and freecad_cmd.exists():
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
model_json,
|
||||
str(fcstd_path),
|
||||
document_name="NACA0016Blade",
|
||||
freecad_cmd=str(freecad_cmd),
|
||||
)
|
||||
print("wrote", fcstd_path)
|
||||
else:
|
||||
print("skipped_fcstd", "FreeCADCmd not found")
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
||||
parser.add_argument(
|
||||
"--freecad-cmd",
|
||||
type=Path,
|
||||
default=DEFAULT_FREECAD_CMD,
|
||||
help="FreeCADCmd path used to write .fcstd; skipped if the path does not exist.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
build_blade(args.output_dir, freecad_cmd=args.freecad_cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Build a hydraulic rod assembly with separate sleeve and piston-rod parts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
OUT_DIR = Path("examples/out/hydraulic_rod_assembly")
|
||||
|
||||
|
||||
def build_hydraulic_rod_assembly():
|
||||
flange_holes = [
|
||||
(0.0, 16.0),
|
||||
(16.0, 0.0),
|
||||
(0.0, -16.0),
|
||||
(-16.0, 0.0),
|
||||
]
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
barrel = scad.make_cylinder_rsolid(
|
||||
radius=16.0,
|
||||
height=120.0,
|
||||
bottom_face_center=(-60.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
rod_gland_flange = scad.make_cylinder_rsolid(
|
||||
radius=22.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(50.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
rod_gland_nose = scad.make_cylinder_rsolid(
|
||||
radius=13.0,
|
||||
height=10.0,
|
||||
bottom_face_center=(58.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
base_cap = scad.make_cylinder_rsolid(
|
||||
radius=18.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(-66.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
rear_eye = scad.make_cylinder_rsolid(
|
||||
radius=14.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(-80.0, -6.0, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
rear_eye_neck = scad.make_box_rsolid(
|
||||
18.0,
|
||||
14.0,
|
||||
16.0,
|
||||
bottom_face_center=(-68.0, 0.0, -8.0),
|
||||
)
|
||||
sleeve_raw = scad.union_rsolid(
|
||||
barrel,
|
||||
rod_gland_flange,
|
||||
rod_gland_nose,
|
||||
base_cap,
|
||||
rear_eye,
|
||||
rear_eye_neck,
|
||||
glue=False,
|
||||
)
|
||||
sleeve_solid = scad.cut_rsolid(
|
||||
sleeve_raw,
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=10.5,
|
||||
height=136.0,
|
||||
bottom_face_center=(-68.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
),
|
||||
)
|
||||
sleeve_solid = scad.cut_rsolid(
|
||||
sleeve_solid,
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=4.6,
|
||||
height=26.0,
|
||||
bottom_face_center=(-80.0, -13.0, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
),
|
||||
)
|
||||
for y, z in flange_holes:
|
||||
sleeve_solid = scad.cut_rsolid(
|
||||
sleeve_solid,
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=1.8,
|
||||
height=16.0,
|
||||
bottom_face_center=(48.0, y, z),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
),
|
||||
)
|
||||
|
||||
piston_land_left = scad.make_cylinder_rsolid(
|
||||
radius=10.0,
|
||||
height=3.2,
|
||||
bottom_face_center=(-6.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
piston_seal_groove = scad.make_cylinder_rsolid(
|
||||
radius=9.0,
|
||||
height=6.0,
|
||||
bottom_face_center=(-3.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
piston_land_right = scad.make_cylinder_rsolid(
|
||||
radius=10.0,
|
||||
height=3.2,
|
||||
bottom_face_center=(2.6, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
chrome_rod = scad.make_cylinder_rsolid(
|
||||
radius=6.5,
|
||||
height=132.0,
|
||||
bottom_face_center=(3.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
rod_eye = scad.make_cylinder_rsolid(
|
||||
radius=13.0,
|
||||
height=9.0,
|
||||
bottom_face_center=(143.0, -4.5, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
rod_eye_neck = scad.make_box_rsolid(
|
||||
20.0,
|
||||
8.0,
|
||||
13.0,
|
||||
bottom_face_center=(130.0, 0.0, -6.5),
|
||||
)
|
||||
piston_rod_raw = scad.union_rsolid(
|
||||
piston_land_left,
|
||||
piston_seal_groove,
|
||||
piston_land_right,
|
||||
chrome_rod,
|
||||
rod_eye,
|
||||
rod_eye_neck,
|
||||
glue=False,
|
||||
)
|
||||
rod_eye_pin_hole = scad.make_cylinder_rsolid(
|
||||
radius=5.5,
|
||||
height=13.0,
|
||||
bottom_face_center=(143.0, -6.5, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
piston_rod_solid = scad.cut_rsolid(piston_rod_raw, rod_eye_pin_hole)
|
||||
|
||||
black_oxide_steel = scad.make_material_rmaterial(
|
||||
"black_oxide_steel",
|
||||
name="Black oxide steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.10, 0.11, 0.12),
|
||||
)
|
||||
chrome_steel = scad.make_material_rmaterial(
|
||||
"chrome_plated_steel",
|
||||
name="Chrome plated steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.78, 0.80, 0.82),
|
||||
)
|
||||
|
||||
sleeve_part = scad.make_part_rpart(
|
||||
"outer_sleeve", sleeve_solid, name="Outer sleeve with clevis and gland"
|
||||
)
|
||||
sleeve_part = scad.assign_material_rpart(sleeve_part, black_oxide_steel)
|
||||
sleeve_faces = ql.faces().resolve(sleeve_solid)
|
||||
sleeve_end_face = None
|
||||
for f in sleeve_faces:
|
||||
n = f.get_normal_at()
|
||||
if abs(abs(n.x) - 1.0) < 0.01 and f.get_area() < 1000.0:
|
||||
sleeve_end_face = f
|
||||
break
|
||||
sleeve_connector = scad.make_face_connector_rconnector("slide_axis", sleeve_end_face)
|
||||
sleeve_part = scad.add_connector_rpart(sleeve_part, sleeve_connector)
|
||||
|
||||
piston_rod_part = scad.make_part_rpart(
|
||||
"piston_rod", piston_rod_solid, name="Inner piston rod with eye end"
|
||||
)
|
||||
piston_rod_part = scad.assign_material_rpart(piston_rod_part, chrome_steel)
|
||||
rod_faces = ql.faces().resolve(piston_rod_solid)
|
||||
rod_end_face = None
|
||||
for f in rod_faces:
|
||||
n = f.get_normal_at()
|
||||
if abs(abs(n.x) - 1.0) < 0.01 and f.get_area() < 1000.0:
|
||||
rod_end_face = f
|
||||
break
|
||||
sleeve_normal = sleeve_end_face.get_normal_at()
|
||||
rod_normal = rod_end_face.get_normal_at()
|
||||
rod_flip = (sleeve_normal.x * rod_normal.x) < 0
|
||||
rod_connector = scad.make_face_connector_rconnector("slide_axis", rod_end_face, flip=rod_flip)
|
||||
piston_rod_part = scad.add_connector_rpart(piston_rod_part, rod_connector)
|
||||
|
||||
hydraulic_assembly = scad.make_assembly_rassembly(
|
||||
"hydraulic_rod_assembly", name="Hydraulic rod assembly"
|
||||
)
|
||||
hydraulic_assembly = scad.add_component_rassembly(
|
||||
hydraulic_assembly,
|
||||
sleeve_part,
|
||||
component_id="outer_sleeve",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
hydraulic_assembly = scad.add_component_rassembly(
|
||||
hydraulic_assembly,
|
||||
piston_rod_part,
|
||||
component_id="inner_piston_rod",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
hydraulic_assembly = scad.ground_component_rassembly(
|
||||
hydraulic_assembly, "outer_sleeve"
|
||||
)
|
||||
hydraulic_assembly = scad.add_prismatic_constraint_rassembly(
|
||||
hydraulic_assembly,
|
||||
"rod_slide",
|
||||
scad.make_connector_ref_rconnectorref("outer_sleeve", "slide_axis"),
|
||||
scad.make_connector_ref_rconnectorref("inner_piston_rod", "slide_axis"),
|
||||
drive_distance=0.0,
|
||||
distance_limit=scad.make_scalar_limit_rscalarlimit(0.0, 100.0),
|
||||
)
|
||||
hydraulic_assembly = scad.solve_assembly_constraints_rassembly(
|
||||
hydraulic_assembly
|
||||
)
|
||||
|
||||
preview = scad.make_compound_from_assembly_rcompound(hydraulic_assembly)
|
||||
model_json = scad.export_model_json(session)
|
||||
|
||||
return hydraulic_assembly, preview, model_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
assembly, preview, model_json = build_hydraulic_rod_assembly()
|
||||
|
||||
model_path = OUT_DIR / "hydraulic_rod_assembly.model.json"
|
||||
step_path = OUT_DIR / "hydraulic_rod_assembly.step"
|
||||
fcstd_path = OUT_DIR / "hydraulic_rod_assembly.FCStd"
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(preview, str(step_path))
|
||||
fcstd_status = "skipped"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(model_json, str(fcstd_path.resolve()))
|
||||
fcstd_status = str(fcstd_path)
|
||||
except Exception as exc: # pragma: no cover - depends on local FreeCAD install
|
||||
fcstd_status = f"skipped ({exc.__class__.__name__})"
|
||||
|
||||
payload = json.loads(model_json)
|
||||
face_count = len(ql.faces().resolve(preview))
|
||||
print("assembly", assembly.assembly_id)
|
||||
print("components", assembly.component_ids())
|
||||
print("preview_solids", len(preview.get_solids()))
|
||||
print("preview_faces", face_count)
|
||||
print("preview_volume", round(preview.get_volume(), 3))
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("wrote", model_path)
|
||||
print("wrote", step_path)
|
||||
print("fcstd", fcstd_status)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Example 11: standalone std.gear internal ring gears.
|
||||
|
||||
This example intentionally avoids planetary assemblies. It exports three
|
||||
separate ring-gear models so the internal tooth profile can be inspected
|
||||
without overlapped sun/planet gears or assembly placement noise:
|
||||
|
||||
- spur internal ring gear
|
||||
- helical internal ring gear
|
||||
- herringbone internal ring gear
|
||||
|
||||
Each model is exported as model JSON, STEP, and FCStd.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
# Ring gear sketches contain many profile entities and produce deep graphs.
|
||||
sys.setrecursionlimit(10000)
|
||||
|
||||
|
||||
MODULE = 1.5
|
||||
RING_TEETH = 66
|
||||
GEAR_HEIGHT = 8.0
|
||||
HELIX_ANGLE = 25.0
|
||||
RIM_THICKNESS = 4.0
|
||||
BACKLASH = 0.08 * MODULE
|
||||
OUTPUT_DIR = Path("examples/out/ring_gears")
|
||||
|
||||
|
||||
def _export_ring(name, description, build_ring):
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
model_path = OUTPUT_DIR / f"{name}.model.json"
|
||||
step_path = OUTPUT_DIR / f"{name}.step"
|
||||
fcstd_path = OUTPUT_DIR / f"{name}.FCStd"
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
ring = build_ring()
|
||||
model_json = scad.export_model_json(session)
|
||||
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(ring, str(step_path))
|
||||
|
||||
fcstd_status = str(fcstd_path)
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(model_json, str(fcstd_path.resolve()))
|
||||
except Exception as exc:
|
||||
fcstd_status = f"skipped ({exc.__class__.__name__})"
|
||||
|
||||
payload = json.loads(model_json)
|
||||
print(f"=== {description} ===")
|
||||
print(f" volume: {ring.get_volume():.1f}")
|
||||
print(f" graph nodes: {len(payload['graph']['nodes'])}")
|
||||
print(f" model: {model_path}")
|
||||
print(f" step: {step_path}")
|
||||
print(f" fcstd: {fcstd_status}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
print(
|
||||
"ring_z={ring_z} module={module} height={height} "
|
||||
"rim={rim} helix={helix} backlash={backlash}".format(
|
||||
ring_z=RING_TEETH,
|
||||
module=MODULE,
|
||||
height=GEAR_HEIGHT,
|
||||
rim=RIM_THICKNESS,
|
||||
helix=HELIX_ANGLE,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
)
|
||||
print(f"output_dir={OUTPUT_DIR}")
|
||||
print()
|
||||
|
||||
_export_ring(
|
||||
"spur_ring_gear",
|
||||
"Spur internal ring gear",
|
||||
lambda: scad.std.gear.make_spur_ring_gear_rsolid(
|
||||
n_teeth=RING_TEETH,
|
||||
module=MODULE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
),
|
||||
)
|
||||
|
||||
_export_ring(
|
||||
"helical_ring_gear",
|
||||
"Helical internal ring gear",
|
||||
lambda: scad.std.gear.make_helical_ring_gear_rsolid(
|
||||
n_teeth=RING_TEETH,
|
||||
module=MODULE,
|
||||
helix_angle=HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
),
|
||||
)
|
||||
|
||||
_export_ring(
|
||||
"herringbone_ring_gear",
|
||||
"Herringbone internal ring gear",
|
||||
lambda: scad.std.gear.make_herringbone_ring_gear_rsolid(
|
||||
n_teeth=RING_TEETH,
|
||||
module=MODULE,
|
||||
helix_angle=HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,636 @@
|
||||
"""Example 12: herringbone planetary reducer carrier assembly.
|
||||
|
||||
This example builds a planetary reducer layout with a fixed internal ring gear:
|
||||
|
||||
- one sun-drive carrier plate with a central shaft
|
||||
- one herringbone sun gear fixed to that sun-drive plate
|
||||
- one fixed herringbone internal ring gear
|
||||
- one upper Y-shaped planet-carrier output plate with three pins
|
||||
- one reusable herringbone planet gear Part instanced three times
|
||||
|
||||
The ring gear is the grounded reference in this static CAD assembly. The sun
|
||||
gear is fixed to the input shaft, while the planet carrier and each planet gear
|
||||
use revolute joints so the product structure reflects the intended power path:
|
||||
sun input -> planet gears against fixed ring -> slower planet-carrier output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
# Gear sketches contain many profile entities and produce deep graphs.
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
|
||||
MODULE = 1.5
|
||||
SUN_TEETH = 18
|
||||
PLANET_TEETH = 24
|
||||
RING_TEETH = SUN_TEETH + 2 * PLANET_TEETH
|
||||
PLANET_COUNT = 3
|
||||
GEAR_HEIGHT = 8.0
|
||||
SUN_HELIX_ANGLE = 25.0
|
||||
PLANET_HELIX_ANGLE = -SUN_HELIX_ANGLE
|
||||
RING_HELIX_ANGLE = PLANET_HELIX_ANGLE
|
||||
RING_RIM_THICKNESS = 5.0
|
||||
RING_BACKLASH = 0.08 * MODULE
|
||||
SUN_PITCH_RADIUS = MODULE * SUN_TEETH / 2.0
|
||||
PLANET_PITCH_RADIUS = MODULE * PLANET_TEETH / 2.0
|
||||
RING_PITCH_RADIUS = MODULE * RING_TEETH / 2.0
|
||||
SUN_BORE_RADIUS = 4.2
|
||||
PLANET_BORE_RADIUS = 3.5
|
||||
SUN_SHAFT_RADIUS = SUN_BORE_RADIUS - 0.2
|
||||
SUN_AXIS_SHOULDER_RADIUS = SUN_BORE_RADIUS - 0.05
|
||||
PLANET_PIN_RADIUS = PLANET_BORE_RADIUS - 0.7
|
||||
PLANET_PIN_BEARING_RADIUS = PLANET_BORE_RADIUS - 0.3
|
||||
SUN_DRIVE_PLATE_RADIUS = 14.0
|
||||
SUN_DRIVE_PLATE_THICKNESS = 4.0
|
||||
SUN_DRIVE_PLATE_BOTTOM_Z = -8.0
|
||||
PLANET_CARRIER_THICKNESS = 3.0
|
||||
PLANET_CARRIER_BOTTOM_Z = GEAR_HEIGHT + 1.0
|
||||
CARRIER_AXIS_CONNECTOR_Z = PLANET_CARRIER_BOTTOM_Z + PLANET_CARRIER_THICKNESS
|
||||
SUN_AXIS_CONNECTOR_Z = GEAR_HEIGHT
|
||||
PLANET_AXIS_CONNECTOR_Z = GEAR_HEIGHT
|
||||
PLANET_PIN_BOTTOM_Z = -0.25
|
||||
PLANET_PIN_TOP_CLEARANCE = 0.5
|
||||
CARRIER_CENTER_CLEARANCE_RADIUS = SUN_BORE_RADIUS + 1.0
|
||||
CARRIER_HUB_RADIUS = CARRIER_CENTER_CLEARANCE_RADIUS + 4.5
|
||||
CARRIER_ARM_WIDTH = 8.0
|
||||
CARRIER_ARM_INNER_CLEARANCE = 0.6
|
||||
CARRIER_ARM_END_OVERHANG = 1.0
|
||||
PLANET_PAD_RADIUS = PLANET_BORE_RADIUS + 4.5
|
||||
SUN_SHAFT_TOP_Z = CARRIER_AXIS_CONNECTOR_Z
|
||||
OUTPUT_DIR = Path("examples/out/herringbone_planetary_gears")
|
||||
|
||||
|
||||
def _planet_spin_angle(carrier_angle_deg: float) -> float:
|
||||
"""Phase each planet so a tooth gap faces the sun contact line."""
|
||||
planet_half_pitch_deg = 180.0 / PLANET_TEETH
|
||||
return carrier_angle_deg + 180.0 - planet_half_pitch_deg
|
||||
|
||||
|
||||
def _z_rotation_placement(origin: tuple[float, float, float], angle_degrees: float):
|
||||
angle_rad = math.radians(angle_degrees)
|
||||
cos_a = math.cos(angle_rad)
|
||||
sin_a = math.sin(angle_rad)
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(cos_a, sin_a, 0.0),
|
||||
y_axis=(-sin_a, cos_a, 0.0),
|
||||
)
|
||||
|
||||
|
||||
def _ground_solid(label: str, solid: scad.Solid) -> None:
|
||||
faces = ql.select(solid.get_faces()).all()
|
||||
tagged_role_faces = ql.select(faces).where(ql.tag("role.*")).all()
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(tagged_role_faces)} "
|
||||
f"volume={solid.get_volume():.1f} tags={','.join(scad.list_tags(solid))}"
|
||||
)
|
||||
|
||||
|
||||
def _ground_compound(label: str, compound: scad.Compound) -> None:
|
||||
solids = ql.select(compound.get_solids()).all()
|
||||
face_count = sum(len(ql.select(solid.get_faces()).all()) for solid in solids)
|
||||
volume = sum(solid.get_volume() for solid in solids)
|
||||
print(f"{label}: solids={len(solids)} faces={face_count} volume={volume:.1f}")
|
||||
|
||||
|
||||
def _axis_face(
|
||||
label: str,
|
||||
solid: scad.Solid,
|
||||
center_xy: tuple[float, float],
|
||||
target_z: float,
|
||||
normal_z: float,
|
||||
) -> scad.Face:
|
||||
candidates = []
|
||||
for face in ql.select(solid.get_faces()).all():
|
||||
normal = face.get_normal_at()
|
||||
if normal_z > 0.0 and normal.z < 0.7:
|
||||
continue
|
||||
if normal_z < 0.0 and normal.z > -0.7:
|
||||
continue
|
||||
center = face.get_center()
|
||||
xy_error = math.hypot(center.x - center_xy[0], center.y - center_xy[1])
|
||||
z_error = abs(center.z - target_z)
|
||||
candidates.append((z_error * 100.0 + xy_error, face, center, normal))
|
||||
|
||||
if not candidates:
|
||||
raise ValueError(f"no connector face found for {label}")
|
||||
|
||||
_score, face, center, normal = min(candidates, key=lambda item: item[0])
|
||||
print(
|
||||
f"{label}_connector_face: center=({center.x:.3f},{center.y:.3f},{center.z:.3f}) "
|
||||
f"normal=({normal.x:.3f},{normal.y:.3f},{normal.z:.3f}) area={face.get_area():.3f}"
|
||||
)
|
||||
return face
|
||||
|
||||
|
||||
def _cut_axial_bore(label: str, solid: scad.Solid, radius: float) -> scad.Solid:
|
||||
cutter = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bored = scad.cut_rsolid(solid, cutter, skip_non_intersecting=False)
|
||||
bored = scad.apply_tag(bored, f"solid.cut.{label}")
|
||||
faces = ql.select(bored.get_faces()).all()
|
||||
print(
|
||||
f"{label}: bore_radius={radius:.2f} faces={len(faces)} "
|
||||
f"volume={bored.get_volume():.1f} tags={','.join(scad.list_tags(bored))}"
|
||||
)
|
||||
return bored
|
||||
|
||||
|
||||
def _build_sun_drive_plate() -> scad.Solid:
|
||||
plate = scad.make_cylinder_rsolid(
|
||||
radius=SUN_DRIVE_PLATE_RADIUS,
|
||||
height=SUN_DRIVE_PLATE_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, SUN_DRIVE_PLATE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=SUN_SHAFT_RADIUS,
|
||||
height=SUN_SHAFT_TOP_Z - SUN_DRIVE_PLATE_BOTTOM_Z,
|
||||
bottom_face_center=(0.0, 0.0, SUN_DRIVE_PLATE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
sun_axis_shoulder = scad.make_cylinder_rsolid(
|
||||
radius=SUN_AXIS_SHOULDER_RADIUS,
|
||||
height=SUN_AXIS_CONNECTOR_Z - SUN_DRIVE_PLATE_BOTTOM_Z,
|
||||
bottom_face_center=(0.0, 0.0, SUN_DRIVE_PLATE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
drive_plate = scad.union_rsolid(plate, sun_axis_shoulder, shaft, glue=False)
|
||||
drive_plate = scad.apply_tag(drive_plate, "role.sun_drive_plate")
|
||||
drive_plate = scad.apply_tag(drive_plate, "group.herringbone_planetary")
|
||||
_ground_solid("sun_drive_plate", drive_plate)
|
||||
return drive_plate
|
||||
|
||||
|
||||
def _build_planet_carrier(planet_center_radius: float) -> scad.Solid:
|
||||
arm_inner_x = CARRIER_CENTER_CLEARANCE_RADIUS + CARRIER_ARM_INNER_CLEARANCE
|
||||
arm_outer_x = planet_center_radius + PLANET_PAD_RADIUS + CARRIER_ARM_END_OVERHANG
|
||||
arm_length = arm_outer_x - arm_inner_x
|
||||
arm_center_x = (arm_inner_x + arm_outer_x) / 2.0
|
||||
pin_height = (
|
||||
PLANET_CARRIER_BOTTOM_Z
|
||||
+ PLANET_CARRIER_THICKNESS
|
||||
+ PLANET_PIN_TOP_CLEARANCE
|
||||
- PLANET_PIN_BOTTOM_Z
|
||||
)
|
||||
hub = scad.make_cylinder_rsolid(
|
||||
radius=CARRIER_HUB_RADIUS,
|
||||
height=PLANET_CARRIER_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, PLANET_CARRIER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
solids = [hub]
|
||||
for index in range(PLANET_COUNT):
|
||||
carrier_angle_deg = 360.0 * index / PLANET_COUNT
|
||||
carrier_angle_rad = math.radians(carrier_angle_deg)
|
||||
arm = scad.make_box_rsolid(
|
||||
CARRIER_ARM_WIDTH,
|
||||
arm_length,
|
||||
PLANET_CARRIER_THICKNESS,
|
||||
bottom_face_center=(arm_center_x, 0.0, PLANET_CARRIER_BOTTOM_Z),
|
||||
)
|
||||
if carrier_angle_deg != 0.0:
|
||||
arm = scad.rotate_shape(
|
||||
arm,
|
||||
carrier_angle_deg,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
solids.append(arm)
|
||||
|
||||
center = (
|
||||
planet_center_radius * math.cos(carrier_angle_rad),
|
||||
planet_center_radius * math.sin(carrier_angle_rad),
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=PLANET_PAD_RADIUS,
|
||||
height=PLANET_CARRIER_THICKNESS,
|
||||
bottom_face_center=(center[0], center[1], PLANET_CARRIER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=PLANET_PIN_RADIUS,
|
||||
height=pin_height,
|
||||
bottom_face_center=(center[0], center[1], PLANET_PIN_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=PLANET_PIN_BEARING_RADIUS,
|
||||
height=PLANET_AXIS_CONNECTOR_Z - PLANET_PIN_BOTTOM_Z,
|
||||
bottom_face_center=(center[0], center[1], PLANET_PIN_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
carrier = scad.union_rsolid(solids, glue=False)
|
||||
carrier = scad.cut_rsolid(
|
||||
carrier,
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=CARRIER_CENTER_CLEARANCE_RADIUS,
|
||||
height=PLANET_CARRIER_THICKNESS + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, PLANET_CARRIER_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
carrier = scad.apply_tag(carrier, "role.planet_carrier")
|
||||
carrier = scad.apply_tag(carrier, "group.herringbone_planetary")
|
||||
print(
|
||||
f"planet_carrier_y_top: arms={PLANET_COUNT} arm_width={CARRIER_ARM_WIDTH:.2f} "
|
||||
f"arm_length={arm_length:.2f} top_z={PLANET_CARRIER_BOTTOM_Z + PLANET_CARRIER_THICKNESS:.2f} "
|
||||
f"hub_radius={CARRIER_HUB_RADIUS:.2f} pad_radius={PLANET_PAD_RADIUS:.2f}"
|
||||
)
|
||||
_ground_solid("planet_carrier", carrier)
|
||||
return carrier
|
||||
|
||||
|
||||
def build_herringbone_planetary_gearset():
|
||||
"""Build the open planetary carrier assembly and return preview/model JSON."""
|
||||
planet_center_radius = MODULE * (SUN_TEETH + PLANET_TEETH) / 2.0
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
sun_drive_plate = _build_sun_drive_plate()
|
||||
planet_carrier = _build_planet_carrier(planet_center_radius)
|
||||
|
||||
ring = scad.std.gear.make_herringbone_ring_gear_rsolid(
|
||||
n_teeth=RING_TEETH,
|
||||
module=MODULE,
|
||||
helix_angle=RING_HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RING_RIM_THICKNESS,
|
||||
backlash=RING_BACKLASH,
|
||||
)
|
||||
ring = scad.apply_tag(ring, "role.fixed_ring_gear")
|
||||
ring = scad.apply_tag(ring, "group.herringbone_planetary")
|
||||
_ground_solid("fixed_ring", ring)
|
||||
|
||||
sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=SUN_TEETH,
|
||||
module=MODULE,
|
||||
helix_angle=SUN_HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
)
|
||||
sun = _cut_axial_bore("sun_bore", sun, SUN_BORE_RADIUS)
|
||||
sun = scad.apply_tag(sun, "role.sun_gear")
|
||||
sun = scad.apply_tag(sun, "group.herringbone_planetary")
|
||||
_ground_solid("sun", sun)
|
||||
|
||||
planet_base = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=PLANET_TEETH,
|
||||
module=MODULE,
|
||||
helix_angle=PLANET_HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
)
|
||||
planet_base = _cut_axial_bore("planet_bore", planet_base, PLANET_BORE_RADIUS)
|
||||
planet_base = scad.apply_tag(planet_base, "role.planet_gear")
|
||||
planet_base = scad.apply_tag(planet_base, "group.herringbone_planetary")
|
||||
_ground_solid("planet_part", planet_base)
|
||||
|
||||
carrier_material = scad.make_material_rmaterial(
|
||||
"matte_anodized_aluminum",
|
||||
name="Matte anodized aluminum",
|
||||
density=2.7e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.28, 0.30, 0.32),
|
||||
)
|
||||
gear_material = scad.make_material_rmaterial(
|
||||
"case_hardened_gear_steel",
|
||||
name="Case hardened gear steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.68, 0.70, 0.72),
|
||||
)
|
||||
print(f"materials: {carrier_material.material_id},{gear_material.material_id}")
|
||||
|
||||
ring_part = scad.make_part_rpart(
|
||||
"fixed_herringbone_ring",
|
||||
ring,
|
||||
name="Fixed herringbone internal ring gear",
|
||||
)
|
||||
ring_part = scad.assign_material_rpart(ring_part, gear_material)
|
||||
ring_part = scad.add_connector_rpart(
|
||||
ring_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"axis",
|
||||
_axis_face("ring_axis", ring, (0.0, 0.0), GEAR_HEIGHT, 1.0),
|
||||
),
|
||||
)
|
||||
|
||||
sun_drive_part = scad.make_part_rpart(
|
||||
"sun_drive_plate",
|
||||
sun_drive_plate,
|
||||
name="Grounded sun-drive plate with central shaft",
|
||||
)
|
||||
sun_drive_part = scad.assign_material_rpart(sun_drive_part, carrier_material)
|
||||
sun_drive_part = scad.add_connector_rpart(
|
||||
sun_drive_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"carrier_axis",
|
||||
_axis_face(
|
||||
"sun_drive_carrier_axis",
|
||||
sun_drive_plate,
|
||||
(0.0, 0.0),
|
||||
CARRIER_AXIS_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
sun_drive_part = scad.add_connector_rpart(
|
||||
sun_drive_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"sun_axis",
|
||||
_axis_face(
|
||||
"sun_drive_sun_axis",
|
||||
sun_drive_plate,
|
||||
(0.0, 0.0),
|
||||
SUN_AXIS_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
carrier_part = scad.make_part_rpart(
|
||||
"planet_carrier",
|
||||
planet_carrier,
|
||||
name="Planet carrier output plate with three pins",
|
||||
)
|
||||
carrier_part = scad.assign_material_rpart(carrier_part, carrier_material)
|
||||
carrier_part = scad.add_connector_rpart(
|
||||
carrier_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"carrier_axis",
|
||||
_axis_face(
|
||||
"planet_carrier_axis",
|
||||
planet_carrier,
|
||||
(0.0, 0.0),
|
||||
CARRIER_AXIS_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
carrier_angle_deg = 360.0 * index / PLANET_COUNT
|
||||
carrier_angle_rad = math.radians(carrier_angle_deg)
|
||||
center_xy = (
|
||||
planet_center_radius * math.cos(carrier_angle_rad),
|
||||
planet_center_radius * math.sin(carrier_angle_rad),
|
||||
)
|
||||
carrier_part = scad.add_connector_rpart(
|
||||
carrier_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
f"planet_{index + 1}_axis",
|
||||
_axis_face(
|
||||
f"carrier_planet_{index + 1}_axis",
|
||||
planet_carrier,
|
||||
center_xy,
|
||||
PLANET_AXIS_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
sun_part = scad.make_part_rpart(
|
||||
"herringbone_sun", sun, name="Herringbone sun gear"
|
||||
)
|
||||
sun_part = scad.assign_material_rpart(sun_part, gear_material)
|
||||
sun_part = scad.add_connector_rpart(
|
||||
sun_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"axis",
|
||||
_axis_face("sun_axis", sun, (0.0, 0.0), GEAR_HEIGHT, 1.0),
|
||||
),
|
||||
)
|
||||
|
||||
planet_part = scad.make_part_rpart(
|
||||
"herringbone_planet", planet_base, name="Reusable herringbone planet gear"
|
||||
)
|
||||
planet_part = scad.assign_material_rpart(planet_part, gear_material)
|
||||
planet_part = scad.add_connector_rpart(
|
||||
planet_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"axis",
|
||||
_axis_face("planet_axis", planet_base, (0.0, 0.0), GEAR_HEIGHT, 1.0),
|
||||
),
|
||||
)
|
||||
print(
|
||||
"parts: "
|
||||
f"{ring_part.part_id},{sun_drive_part.part_id},{carrier_part.part_id},"
|
||||
f"{sun_part.part_id},{planet_part.part_id}"
|
||||
)
|
||||
|
||||
gearset = scad.make_assembly_rassembly(
|
||||
"herringbone_planetary_gearset",
|
||||
name="Fixed-ring herringbone planetary reducer assembly",
|
||||
)
|
||||
gearset = scad.add_component_rassembly(
|
||||
gearset,
|
||||
ring_part,
|
||||
component_id="fixed_ring",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Grounded fixed internal ring gear",
|
||||
)
|
||||
gearset = scad.add_component_rassembly(
|
||||
gearset,
|
||||
sun_drive_part,
|
||||
component_id="sun_drive_plate",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Grounded sun-drive input plate",
|
||||
)
|
||||
gearset = scad.add_component_rassembly(
|
||||
gearset,
|
||||
carrier_part,
|
||||
component_id="planet_carrier",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Planet-carrier output plate",
|
||||
)
|
||||
gearset = scad.add_component_rassembly(
|
||||
gearset,
|
||||
sun_part,
|
||||
component_id="sun",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Sun gear fixed to input plate",
|
||||
)
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
carrier_angle_deg = 360.0 * index / PLANET_COUNT
|
||||
carrier_angle_rad = math.radians(carrier_angle_deg)
|
||||
center = (
|
||||
planet_center_radius * math.cos(carrier_angle_rad),
|
||||
planet_center_radius * math.sin(carrier_angle_rad),
|
||||
0.0,
|
||||
)
|
||||
spin_angle = _planet_spin_angle(carrier_angle_deg)
|
||||
gearset = scad.add_component_rassembly(
|
||||
gearset,
|
||||
planet_part,
|
||||
component_id=f"planet_{index + 1}",
|
||||
placement=_z_rotation_placement(center, spin_angle),
|
||||
name=f"Planet gear {index + 1}",
|
||||
)
|
||||
print(
|
||||
f"planet_{index + 1}: carrier={carrier_angle_deg:.1f}deg "
|
||||
f"center=({center[0]:.3f},{center[1]:.3f},{center[2]:.3f}) "
|
||||
f"spin={spin_angle:.1f}deg"
|
||||
)
|
||||
|
||||
gearset = scad.ground_component_rassembly(gearset, "fixed_ring")
|
||||
gearset = scad.add_revolute_constraint_rassembly(
|
||||
gearset,
|
||||
"sun_input_revolute",
|
||||
scad.make_connector_ref_rconnectorref("fixed_ring", "axis"),
|
||||
scad.make_connector_ref_rconnectorref("sun_drive_plate", "sun_axis"),
|
||||
name="Sun input shaft rotates inside the fixed ring gear",
|
||||
)
|
||||
gearset = scad.add_revolute_constraint_rassembly(
|
||||
gearset,
|
||||
"carrier_output_revolute",
|
||||
scad.make_connector_ref_rconnectorref("sun_drive_plate", "carrier_axis"),
|
||||
scad.make_connector_ref_rconnectorref("planet_carrier", "carrier_axis"),
|
||||
name="Planet carrier rotates around the sun-drive plate axis",
|
||||
)
|
||||
gearset = scad.add_fixed_constraint_rassembly(
|
||||
gearset,
|
||||
"sun_fixed_to_drive_plate",
|
||||
scad.make_connector_ref_rconnectorref("sun_drive_plate", "sun_axis"),
|
||||
scad.make_connector_ref_rconnectorref("sun", "axis"),
|
||||
name="Sun gear fixed to the input shaft",
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
gearset = scad.add_revolute_constraint_rassembly(
|
||||
gearset,
|
||||
f"planet_{index + 1}_revolute",
|
||||
scad.make_connector_ref_rconnectorref("planet_carrier", f"planet_{index + 1}_axis"),
|
||||
scad.make_connector_ref_rconnectorref(f"planet_{index + 1}", "axis"),
|
||||
name=f"Planet gear {index + 1} rotates on its carrier pin",
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_ref = scad.make_connector_ref_rconnectorref(
|
||||
component_id=f"planet_{index + 1}",
|
||||
connector_id="axis",
|
||||
)
|
||||
gearset = scad.add_gear_constraint_rassembly(
|
||||
assembly=gearset,
|
||||
constraint_id=f"sun_planet_{index + 1}_external_mesh",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="sun_drive_plate",
|
||||
connector_id="sun_axis",
|
||||
),
|
||||
connector_b=planet_ref,
|
||||
pitch_radius_a=SUN_PITCH_RADIUS,
|
||||
pitch_radius_b=PLANET_PITCH_RADIUS,
|
||||
name=f"External sun to planet {index + 1} gear mesh",
|
||||
)
|
||||
gearset = scad.add_belt_constraint_rassembly(
|
||||
assembly=gearset,
|
||||
constraint_id=f"ring_planet_{index + 1}_internal_mesh",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="fixed_ring",
|
||||
connector_id="axis",
|
||||
),
|
||||
connector_b=planet_ref,
|
||||
pulley_radius_a=RING_PITCH_RADIUS,
|
||||
pulley_radius_b=PLANET_PITCH_RADIUS,
|
||||
name=f"Internal fixed-ring to planet {index + 1} gear mesh",
|
||||
)
|
||||
print(
|
||||
"gear_constraints: "
|
||||
f"sun_planet_external={PLANET_COUNT} ring_planet_internal={PLANET_COUNT} "
|
||||
f"radii=({SUN_PITCH_RADIUS:.3f},{PLANET_PITCH_RADIUS:.3f},{RING_PITCH_RADIUS:.3f})"
|
||||
)
|
||||
gearset = scad.solve_assembly_constraints_rassembly(gearset)
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(gearset)
|
||||
print(
|
||||
"assembly: "
|
||||
f"components={','.join(gearset.component_ids())} "
|
||||
f"grounded={','.join(gearset.grounded_component_ids)} "
|
||||
f"solved={report.solved} constraints={len(gearset.constraints)}"
|
||||
)
|
||||
for residual in report.residuals:
|
||||
print(
|
||||
f"constraint_{residual.constraint_id}: "
|
||||
f"translation={residual.translation_error:.6g} "
|
||||
f"angle={residual.angular_error_degrees:.6g} "
|
||||
f"ok={residual.within_tolerance}"
|
||||
)
|
||||
|
||||
preview = scad.make_compound_from_assembly_rcompound(gearset)
|
||||
_ground_compound("assembly_preview", preview)
|
||||
model_json = scad.export_model_json(session)
|
||||
|
||||
return gearset, preview, model_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
model_path = OUTPUT_DIR / "herringbone_planetary_gearset.model.json"
|
||||
step_path = OUTPUT_DIR / "herringbone_planetary_gearset.step"
|
||||
fcstd_path = OUTPUT_DIR / "herringbone_planetary_gearset.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json = build_herringbone_planetary_gearset()
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(preview, str(step_path))
|
||||
|
||||
fcstd_status = "not attempted"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(model_json, str(fcstd_path.resolve()))
|
||||
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"failed ({exc.__class__.__name__}: {exc})"
|
||||
|
||||
payload = json.loads(model_json)
|
||||
replayed = scad.replay_model_json(model_json)
|
||||
solids = ql.select(preview.get_solids()).all()
|
||||
face_count = sum(len(ql.select(solid.get_faces()).all()) for solid in solids)
|
||||
volumes = [solid.get_volume() for solid in solids]
|
||||
|
||||
print(
|
||||
"sun_z={sun} planet_z={planet} ring_z={ring} planets={count} "
|
||||
"module={module} height={height} sun_helix={sun_helix} planet_helix={planet_helix}".format(
|
||||
sun=SUN_TEETH,
|
||||
planet=PLANET_TEETH,
|
||||
ring=RING_TEETH,
|
||||
count=PLANET_COUNT,
|
||||
module=MODULE,
|
||||
height=GEAR_HEIGHT,
|
||||
sun_helix=SUN_HELIX_ANGLE,
|
||||
planet_helix=PLANET_HELIX_ANGLE,
|
||||
)
|
||||
)
|
||||
print(f"planet_center_radius={MODULE * (SUN_TEETH + PLANET_TEETH) / 2.0:.3f}")
|
||||
print(f"assembly={assembly.assembly_id}")
|
||||
print("components=" + ",".join(assembly.component_ids()))
|
||||
print(f"preview_solids={len(solids)}")
|
||||
print(f"preview_faces={face_count}")
|
||||
print("volumes=" + ",".join(f"{volume:.1f}" for volume in volumes))
|
||||
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"step={step_path}")
|
||||
print(f"fcstd={fcstd_status}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,847 @@
|
||||
"""Example 13: compact 50 mm x 10 mm cycloidal reducer assembly.
|
||||
|
||||
Design plan
|
||||
===========
|
||||
|
||||
Package envelope:
|
||||
- Maximum outside diameter: 50 mm.
|
||||
- Maximum stack height: 10 mm.
|
||||
|
||||
Reduction stage:
|
||||
- Fixed pin ring: 11 pins.
|
||||
- Twin cycloidal discs: 10 lobes each, stacked with 180 degree eccentric
|
||||
carrier separation and a half-lobe tooth-index phase for load balance.
|
||||
- Single-stage reduction: 11 - 1 = 10:1.
|
||||
- The fixed pin ring contact is represented as a gear-like coupling between the
|
||||
input eccentric carrier and each cycloidal disc's relative spin: each disc
|
||||
spins -11/10 turn relative to its eccentric carrier for each input turn,
|
||||
giving a global cycloidal/output phase of -1/10 input turn.
|
||||
|
||||
Structure:
|
||||
- fixed_housing: outer sleeve, top/bottom retainers, and 11 fixed ring pins.
|
||||
- input_disk: bottom three-hole threaded mounting disk plus a double eccentric
|
||||
cam shaft. The lower cam is at 0 degrees; the upper cam is at 180 degrees.
|
||||
- lower_cycloidal_disc and upper_cycloidal_disc: 10-lobed discs with eccentric
|
||||
bearing bores and three oversize output-pin relief holes. The upper disc is
|
||||
tooth-indexed by half a lobe, i.e. 180 degrees divided by 10 lobes = 18
|
||||
geometric degrees. A full 180 degree rotation would be symmetry-equivalent
|
||||
to the lower disc because the profile has 10 lobes.
|
||||
- output_disk: top three-hole threaded mounting disk plus three output pins.
|
||||
|
||||
Assembly relationships:
|
||||
- fixed_housing is grounded.
|
||||
- input_disk is revolute about the housing axis.
|
||||
- output_disk is revolute about the housing axis.
|
||||
- lower_cycloidal_disc is revolute on the lower input eccentric cam axis.
|
||||
- upper_cycloidal_disc is revolute on the upper input eccentric cam axis.
|
||||
- input_disk to each cycloidal disc has a gear-like pin-ring rolling coupling.
|
||||
|
||||
The output pins are fixed to the output disk and pass through oversize circular
|
||||
holes in both cycloidal discs. The two discs load those pins from opposite
|
||||
eccentric directions, so the real mechanism keeps the output-pin side load more
|
||||
balanced through a full rotation. This SDK does not yet have a native
|
||||
pin-slot/contact primitive, so the example models that relation as clearance
|
||||
geometry rather than a false coaxial ratio shortcut.
|
||||
|
||||
Each cycloidal outline is fit as ten cubic B-spline segments, one segment per
|
||||
lobe. This keeps the exported topology small and stable while preserving the
|
||||
analytic pin-wheel profile within a controlled fit tolerance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
|
||||
PACKAGE_DIAMETER = 50.0
|
||||
PACKAGE_RADIUS = PACKAGE_DIAMETER / 2.0
|
||||
PACKAGE_HEIGHT = 10.0
|
||||
OUTPUT_DIR = Path("examples/out/cycloidal_reducer_50mm_10x")
|
||||
|
||||
PIN_COUNT = 11
|
||||
CYCLOID_LOBES = PIN_COUNT - 1
|
||||
REDUCTION_RATIO = CYCLOID_LOBES
|
||||
ECCENTRICITY = 0.8
|
||||
LOWER_ECCENTRIC_CENTER = (ECCENTRICITY, 0.0)
|
||||
UPPER_ECCENTRIC_CENTER = (-ECCENTRICITY, 0.0)
|
||||
UPPER_CYCLOID_BODY_PHASE_DEGREES = 180.0 / CYCLOID_LOBES
|
||||
|
||||
RING_PIN_PITCH_RADIUS = 18.0
|
||||
RING_PIN_RADIUS = 0.65
|
||||
PROFILE_ROLLER_RADIUS = 1.6
|
||||
HOUSING_INNER_RADIUS = 22.3
|
||||
RETAINER_INNER_RADIUS = 13.5
|
||||
|
||||
BOTTOM_RETAINER_BOTTOM_Z = 1.25
|
||||
RETAINER_THICKNESS = 0.75
|
||||
TOP_RETAINER_BOTTOM_Z = 8.0
|
||||
PIN_BOTTOM_Z = BOTTOM_RETAINER_BOTTOM_Z
|
||||
PIN_TOP_Z = TOP_RETAINER_BOTTOM_Z + RETAINER_THICKNESS
|
||||
|
||||
INPUT_FLANGE_RADIUS = 11.8
|
||||
OUTPUT_FLANGE_RADIUS = 11.8
|
||||
FLANGE_THICKNESS = 1.1
|
||||
INPUT_FLANGE_BOTTOM_Z = 0.0
|
||||
OUTPUT_FLANGE_BOTTOM_Z = PACKAGE_HEIGHT - FLANGE_THICKNESS
|
||||
MOUNT_HOLE_COUNT = 3
|
||||
MOUNT_HOLE_RADIUS = 1.03
|
||||
MOUNT_HOLE_ENTRY_RADIUS = 1.35
|
||||
MOUNT_HOLE_ENTRY_DEPTH = 0.28
|
||||
MOUNT_HOLE_PITCH_RADIUS = 8.8
|
||||
|
||||
ECCENTRIC_BOSS_RADIUS = 3.1
|
||||
INPUT_SHAFT_RADIUS = 0.55
|
||||
INPUT_CAM_DATUM_PAD_RADIUS = 0.22
|
||||
INPUT_CAM_DATUM_PAD_HEIGHT = 0.06
|
||||
INPUT_CAM_DATUM_PAD_OVERLAP = 0.02
|
||||
CYCLOID_BORE_RADIUS = 3.45
|
||||
LOWER_CYCLOID_BOTTOM_Z = 2.15
|
||||
CYCLOID_DISC_HEIGHT = 2.65
|
||||
CYCLOID_DISC_GAP = 0.20
|
||||
CYCLOID_BEARING_RACE_HEIGHT = 0.10
|
||||
LOWER_CYCLOID_TOP_Z = LOWER_CYCLOID_BOTTOM_Z + CYCLOID_DISC_HEIGHT
|
||||
LOWER_CYCLOID_CONNECTOR_Z = LOWER_CYCLOID_TOP_Z + CYCLOID_BEARING_RACE_HEIGHT
|
||||
UPPER_CYCLOID_BOTTOM_Z = LOWER_CYCLOID_CONNECTOR_Z + CYCLOID_DISC_GAP
|
||||
UPPER_CYCLOID_TOP_Z = UPPER_CYCLOID_BOTTOM_Z + CYCLOID_DISC_HEIGHT
|
||||
UPPER_CYCLOID_CONNECTOR_Z = UPPER_CYCLOID_TOP_Z + CYCLOID_BEARING_RACE_HEIGHT
|
||||
CYCLOID_STACK_HEIGHT = UPPER_CYCLOID_CONNECTOR_Z - LOWER_CYCLOID_BOTTOM_Z
|
||||
CYCLOID_LOBE_SAMPLE_COUNT = 33
|
||||
CYCLOID_SPLINE_TOLERANCE = 0.005
|
||||
CYCLOID_SPLINE_MAX_CONTROL_POINTS = 20
|
||||
|
||||
OUTPUT_PIN_COUNT = 3
|
||||
OUTPUT_PIN_RADIUS = 1.0
|
||||
OUTPUT_PIN_CLEARANCE_RADIUS = OUTPUT_PIN_RADIUS + ECCENTRICITY + 0.25
|
||||
OUTPUT_PIN_PITCH_RADIUS = 6.4
|
||||
OUTPUT_PIN_PHASE = 60.0
|
||||
OUTPUT_PIN_BOTTOM_Z = LOWER_CYCLOID_BOTTOM_Z
|
||||
OUTPUT_PIN_TOP_Z = OUTPUT_FLANGE_BOTTOM_Z + 0.20
|
||||
|
||||
|
||||
def _polar(radius: float, angle_degrees: float) -> tuple[float, float]:
|
||||
angle = math.radians(angle_degrees)
|
||||
return radius * math.cos(angle), radius * math.sin(angle)
|
||||
|
||||
|
||||
def _z_rotation_placement(origin: tuple[float, float, float], angle_degrees: float):
|
||||
angle = math.radians(angle_degrees)
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(math.cos(angle), math.sin(angle), 0.0),
|
||||
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
|
||||
)
|
||||
|
||||
|
||||
def _ground_solid(label: str, solid: scad.Solid) -> None:
|
||||
faces = ql.select(solid.get_faces()).all()
|
||||
role_faces = ql.select(faces).where(ql.tag("role.*")).all()
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
|
||||
f"volume={solid.get_volume():.1f} tags={','.join(scad.list_tags(solid))}"
|
||||
)
|
||||
|
||||
|
||||
def _ground_compound(label: str, compound: scad.Compound) -> None:
|
||||
solids = ql.select(compound.get_solids()).all()
|
||||
face_count = sum(len(ql.select(solid.get_faces()).all()) for solid in solids)
|
||||
volume = sum(solid.get_volume() for solid in solids)
|
||||
print(f"{label}: solids={len(solids)} faces={face_count} volume={volume:.1f}")
|
||||
|
||||
|
||||
def _axis_face(
|
||||
label: str,
|
||||
solid: scad.Solid,
|
||||
center_xy: tuple[float, float],
|
||||
target_z: float,
|
||||
normal_z: float,
|
||||
) -> scad.Face:
|
||||
candidates = []
|
||||
for face in ql.select(solid.get_faces()).all():
|
||||
normal = face.get_normal_at()
|
||||
if normal_z > 0.0 and normal.z < 0.7:
|
||||
continue
|
||||
if normal_z < 0.0 and normal.z > -0.7:
|
||||
continue
|
||||
center = face.get_center()
|
||||
xy_error = math.hypot(center.x - center_xy[0], center.y - center_xy[1])
|
||||
z_error = abs(center.z - target_z)
|
||||
candidates.append((z_error * 100.0 + xy_error, face, center, normal))
|
||||
|
||||
if not candidates:
|
||||
raise ValueError(f"no connector face found for {label}")
|
||||
|
||||
_score, face, center, normal = min(candidates, key=lambda item: item[0])
|
||||
print(
|
||||
f"{label}_connector_face: center=({center.x:.3f},{center.y:.3f},{center.z:.3f}) "
|
||||
f"normal=({normal.x:.3f},{normal.y:.3f},{normal.z:.3f}) area={face.get_area():.3f}"
|
||||
)
|
||||
return face
|
||||
|
||||
|
||||
def _make_annular_cylinder(
|
||||
*,
|
||||
outer_radius: float,
|
||||
inner_radius: float,
|
||||
bottom_z: float,
|
||||
height: float,
|
||||
) -> scad.Solid:
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=outer_radius,
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
inner = scad.make_cylinder_rsolid(
|
||||
radius=inner_radius,
|
||||
height=height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
return scad.cut_rsolid(outer, inner, skip_non_intersecting=False)
|
||||
|
||||
|
||||
def _cut_three_threaded_hole_envelopes(
|
||||
solid: scad.Solid,
|
||||
*,
|
||||
bottom_z: float,
|
||||
thickness: float,
|
||||
entry_face: str,
|
||||
phase_degrees: float = 0.0,
|
||||
) -> scad.Solid:
|
||||
cutters: list[scad.Solid] = []
|
||||
for index in range(MOUNT_HOLE_COUNT):
|
||||
angle = phase_degrees + 360.0 * index / MOUNT_HOLE_COUNT
|
||||
x, y = _polar(MOUNT_HOLE_PITCH_RADIUS, angle)
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=MOUNT_HOLE_RADIUS,
|
||||
height=thickness + 0.4,
|
||||
bottom_face_center=(x, y, bottom_z - 0.2),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
if entry_face == "bottom":
|
||||
entry_bottom_z = bottom_z - 0.04
|
||||
elif entry_face == "top":
|
||||
entry_bottom_z = bottom_z + thickness - MOUNT_HOLE_ENTRY_DEPTH
|
||||
else:
|
||||
raise ValueError("entry_face must be 'bottom' or 'top'")
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=MOUNT_HOLE_ENTRY_RADIUS,
|
||||
height=MOUNT_HOLE_ENTRY_DEPTH + 0.08,
|
||||
bottom_face_center=(x, y, entry_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
return scad.cut_rsolid(solid, cutters, skip_non_intersecting=False)
|
||||
|
||||
|
||||
def _build_fixed_housing() -> scad.Solid:
|
||||
sleeve = _make_annular_cylinder(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=HOUSING_INNER_RADIUS,
|
||||
bottom_z=0.0,
|
||||
height=PACKAGE_HEIGHT,
|
||||
)
|
||||
bottom_retainer = _make_annular_cylinder(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=RETAINER_INNER_RADIUS,
|
||||
bottom_z=BOTTOM_RETAINER_BOTTOM_Z,
|
||||
height=RETAINER_THICKNESS,
|
||||
)
|
||||
top_retainer = _make_annular_cylinder(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=RETAINER_INNER_RADIUS,
|
||||
bottom_z=TOP_RETAINER_BOTTOM_Z,
|
||||
height=RETAINER_THICKNESS,
|
||||
)
|
||||
pins: list[scad.Solid] = []
|
||||
for index in range(PIN_COUNT):
|
||||
angle = 360.0 * index / PIN_COUNT
|
||||
x, y = _polar(RING_PIN_PITCH_RADIUS, angle)
|
||||
pins.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=RING_PIN_RADIUS,
|
||||
height=PIN_TOP_Z - PIN_BOTTOM_Z,
|
||||
bottom_face_center=(x, y, PIN_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
housing = scad.union_rsolid(
|
||||
sleeve,
|
||||
bottom_retainer,
|
||||
top_retainer,
|
||||
pins,
|
||||
glue=False,
|
||||
)
|
||||
housing = scad.apply_tag(housing, "role.fixed_pin_housing")
|
||||
housing = scad.apply_tag(housing, "group.cycloidal_reducer")
|
||||
_ground_solid("fixed_housing", housing)
|
||||
return housing
|
||||
|
||||
|
||||
def _build_input_disk() -> scad.Solid:
|
||||
flange = scad.make_cylinder_rsolid(
|
||||
radius=INPUT_FLANGE_RADIUS,
|
||||
height=FLANGE_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, INPUT_FLANGE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
flange = _cut_three_threaded_hole_envelopes(
|
||||
flange,
|
||||
bottom_z=INPUT_FLANGE_BOTTOM_Z,
|
||||
thickness=FLANGE_THICKNESS,
|
||||
entry_face="bottom",
|
||||
phase_degrees=0.0,
|
||||
)
|
||||
cam_bottom_z = FLANGE_THICKNESS - 0.20
|
||||
input_shaft = scad.make_cylinder_rsolid(
|
||||
radius=INPUT_SHAFT_RADIUS,
|
||||
height=UPPER_CYCLOID_CONNECTOR_Z - cam_bottom_z,
|
||||
bottom_face_center=(0.0, 0.0, cam_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
lower_pad_bottom_z = LOWER_CYCLOID_CONNECTOR_Z - INPUT_CAM_DATUM_PAD_HEIGHT
|
||||
lower_eccentric_boss = scad.make_cylinder_rsolid(
|
||||
radius=ECCENTRIC_BOSS_RADIUS,
|
||||
height=lower_pad_bottom_z + INPUT_CAM_DATUM_PAD_OVERLAP - cam_bottom_z,
|
||||
bottom_face_center=(*LOWER_ECCENTRIC_CENTER, cam_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
lower_datum_pad = scad.make_cylinder_rsolid(
|
||||
radius=INPUT_CAM_DATUM_PAD_RADIUS,
|
||||
height=INPUT_CAM_DATUM_PAD_HEIGHT,
|
||||
bottom_face_center=(*LOWER_ECCENTRIC_CENTER, lower_pad_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
upper_boss_bottom_z = UPPER_CYCLOID_BOTTOM_Z - 0.10
|
||||
upper_pad_bottom_z = UPPER_CYCLOID_CONNECTOR_Z - INPUT_CAM_DATUM_PAD_HEIGHT
|
||||
upper_eccentric_boss = scad.make_cylinder_rsolid(
|
||||
radius=ECCENTRIC_BOSS_RADIUS,
|
||||
height=upper_pad_bottom_z + INPUT_CAM_DATUM_PAD_OVERLAP - upper_boss_bottom_z,
|
||||
bottom_face_center=(*UPPER_ECCENTRIC_CENTER, upper_boss_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
upper_datum_pad = scad.make_cylinder_rsolid(
|
||||
radius=INPUT_CAM_DATUM_PAD_RADIUS,
|
||||
height=INPUT_CAM_DATUM_PAD_HEIGHT,
|
||||
bottom_face_center=(*UPPER_ECCENTRIC_CENTER, upper_pad_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
input_disk = scad.union_rsolid(
|
||||
flange,
|
||||
input_shaft,
|
||||
lower_eccentric_boss,
|
||||
lower_datum_pad,
|
||||
upper_eccentric_boss,
|
||||
upper_datum_pad,
|
||||
glue=False,
|
||||
)
|
||||
input_disk = scad.apply_tag(input_disk, "role.input_three_thread_disk")
|
||||
input_disk = scad.apply_tag(input_disk, "role.double_eccentric_camshaft")
|
||||
input_disk = scad.apply_tag(input_disk, "group.cycloidal_reducer")
|
||||
_ground_solid("input_disk", input_disk)
|
||||
return input_disk
|
||||
|
||||
|
||||
def _build_cycloidal_disc(
|
||||
*,
|
||||
label: str,
|
||||
bottom_z: float,
|
||||
output_pin_phase: float,
|
||||
body_phase_degrees: float,
|
||||
role_tag: str,
|
||||
) -> scad.Solid:
|
||||
disc = scad.std.gear.make_cycloidal_disc_rsolid(
|
||||
n_lobes=CYCLOID_LOBES,
|
||||
ring_pin_pitch_radius=RING_PIN_PITCH_RADIUS,
|
||||
roller_radius=PROFILE_ROLLER_RADIUS,
|
||||
eccentricity=ECCENTRICITY,
|
||||
gear_height=CYCLOID_DISC_HEIGHT,
|
||||
bore_radius=CYCLOID_BORE_RADIUS,
|
||||
output_pin_count=OUTPUT_PIN_COUNT,
|
||||
output_pin_pitch_radius=OUTPUT_PIN_PITCH_RADIUS,
|
||||
output_pin_clearance_radius=OUTPUT_PIN_CLEARANCE_RADIUS,
|
||||
output_pin_phase=output_pin_phase,
|
||||
sample_count_per_lobe=CYCLOID_LOBE_SAMPLE_COUNT,
|
||||
spline_tolerance=CYCLOID_SPLINE_TOLERANCE,
|
||||
max_control_points=CYCLOID_SPLINE_MAX_CONTROL_POINTS,
|
||||
)
|
||||
cycloid_meta = disc.get_metadata("std.gear.cycloidal_disc", {})
|
||||
top_z = bottom_z + CYCLOID_DISC_HEIGHT
|
||||
connector_z = top_z + CYCLOID_BEARING_RACE_HEIGHT
|
||||
disc = scad.translate_shape(disc, (0.0, 0.0, bottom_z))
|
||||
bearing_race = _make_annular_cylinder(
|
||||
outer_radius=CYCLOID_BORE_RADIUS + 0.75,
|
||||
inner_radius=CYCLOID_BORE_RADIUS,
|
||||
bottom_z=top_z - 0.02,
|
||||
height=CYCLOID_BEARING_RACE_HEIGHT + 0.02,
|
||||
)
|
||||
disc = scad.union_rsolid(disc, bearing_race, glue=False)
|
||||
if body_phase_degrees:
|
||||
disc = scad.rotate_shape(
|
||||
disc,
|
||||
body_phase_degrees,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
disc = scad.apply_tag(disc, role_tag)
|
||||
disc = scad.apply_tag(disc, "role.ten_lobe_cycloidal_disc")
|
||||
disc = scad.apply_tag(disc, "group.cycloidal_reducer")
|
||||
print(
|
||||
f"{label}_profile: "
|
||||
f"pins={PIN_COUNT} lobes={CYCLOID_LOBES} "
|
||||
f"bottom_z={bottom_z:.2f} connector_z={connector_z:.2f} "
|
||||
f"body_phase={body_phase_degrees:.1f} "
|
||||
f"raw_output_pin_phase={output_pin_phase:.1f} "
|
||||
f"segments={cycloid_meta.get('segment_count', CYCLOID_LOBES)} "
|
||||
f"samples_per_lobe={CYCLOID_LOBE_SAMPLE_COUNT} "
|
||||
f"control_points={min(cycloid_meta.get('control_point_counts', [0]))}.."
|
||||
f"{max(cycloid_meta.get('control_point_counts', [0]))} "
|
||||
f"fit_error_max={max(cycloid_meta.get('max_errors', [0.0])):.5f} "
|
||||
f"radius_min={cycloid_meta.get('radius_min', 0.0):.3f} "
|
||||
f"radius_max={cycloid_meta.get('radius_max', 0.0):.3f}"
|
||||
)
|
||||
_ground_solid(label, disc)
|
||||
return disc
|
||||
|
||||
|
||||
def _build_output_disk() -> scad.Solid:
|
||||
flange = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_RADIUS,
|
||||
height=FLANGE_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
flange = _cut_three_threaded_hole_envelopes(
|
||||
flange,
|
||||
bottom_z=OUTPUT_FLANGE_BOTTOM_Z,
|
||||
thickness=FLANGE_THICKNESS,
|
||||
entry_face="top",
|
||||
phase_degrees=0.0,
|
||||
)
|
||||
pins: list[scad.Solid] = []
|
||||
for index in range(OUTPUT_PIN_COUNT):
|
||||
angle = OUTPUT_PIN_PHASE + 360.0 * index / OUTPUT_PIN_COUNT
|
||||
x, y = _polar(OUTPUT_PIN_PITCH_RADIUS, angle)
|
||||
pins.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_PIN_RADIUS,
|
||||
height=OUTPUT_PIN_TOP_Z - OUTPUT_PIN_BOTTOM_Z,
|
||||
bottom_face_center=(x, y, OUTPUT_PIN_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
output_disk = scad.union_rsolid(flange, pins, glue=False)
|
||||
output_disk = scad.apply_tag(output_disk, "role.output_three_thread_disk")
|
||||
output_disk = scad.apply_tag(output_disk, "group.cycloidal_reducer")
|
||||
_ground_solid("output_disk", output_disk)
|
||||
return output_disk
|
||||
|
||||
|
||||
def build_cycloidal_reducer():
|
||||
with scad.GraphSession() as session:
|
||||
housing = _build_fixed_housing()
|
||||
input_disk = _build_input_disk()
|
||||
lower_cycloidal_disc = _build_cycloidal_disc(
|
||||
label="lower_cycloidal_disc",
|
||||
bottom_z=LOWER_CYCLOID_BOTTOM_Z,
|
||||
output_pin_phase=OUTPUT_PIN_PHASE,
|
||||
body_phase_degrees=0.0,
|
||||
role_tag="role.lower_cycloidal_disc",
|
||||
)
|
||||
upper_cycloidal_disc = _build_cycloidal_disc(
|
||||
label="upper_cycloidal_disc",
|
||||
bottom_z=UPPER_CYCLOID_BOTTOM_Z,
|
||||
output_pin_phase=OUTPUT_PIN_PHASE - UPPER_CYCLOID_BODY_PHASE_DEGREES,
|
||||
body_phase_degrees=UPPER_CYCLOID_BODY_PHASE_DEGREES,
|
||||
role_tag="role.upper_cycloidal_disc",
|
||||
)
|
||||
output_disk = _build_output_disk()
|
||||
|
||||
housing_material = scad.make_material_rmaterial(
|
||||
material_id="black_anodized_aluminum",
|
||||
name="Black anodized aluminum",
|
||||
density=2.7e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.08, 0.08, 0.09),
|
||||
)
|
||||
steel_material = scad.make_material_rmaterial(
|
||||
material_id="bearing_steel",
|
||||
name="Bearing steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.62, 0.64, 0.66),
|
||||
)
|
||||
bronze_material = scad.make_material_rmaterial(
|
||||
material_id="phosphor_bronze",
|
||||
name="Phosphor bronze",
|
||||
density=8.8e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.72, 0.48, 0.20),
|
||||
)
|
||||
print(
|
||||
"materials: "
|
||||
f"{housing_material.material_id},{steel_material.material_id},{bronze_material.material_id}"
|
||||
)
|
||||
|
||||
housing_part = scad.make_part_rpart(
|
||||
part_id="fixed_pin_housing",
|
||||
body=housing,
|
||||
name="Fixed housing with eleven pin ring",
|
||||
)
|
||||
housing_part = scad.assign_material_rpart(housing_part, housing_material)
|
||||
housing_part = scad.add_connector_rpart(
|
||||
housing_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"input_axis",
|
||||
_axis_face(
|
||||
"housing_input_axis",
|
||||
housing,
|
||||
(0.0, 0.0),
|
||||
INPUT_FLANGE_BOTTOM_Z,
|
||||
-1.0,
|
||||
),
|
||||
flip=True,
|
||||
),
|
||||
)
|
||||
housing_part = scad.add_connector_rpart(
|
||||
housing_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"output_axis",
|
||||
_axis_face(
|
||||
"housing_output_axis",
|
||||
housing,
|
||||
(0.0, 0.0),
|
||||
PACKAGE_HEIGHT,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
input_part = scad.make_part_rpart(
|
||||
part_id="input_three_thread_disk",
|
||||
body=input_disk,
|
||||
name="Input three threaded-hole disk with double eccentric camshaft",
|
||||
)
|
||||
input_part = scad.assign_material_rpart(input_part, steel_material)
|
||||
input_part = scad.add_connector_rpart(
|
||||
input_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"axis",
|
||||
_axis_face(
|
||||
"input_axis",
|
||||
input_disk,
|
||||
(0.0, 0.0),
|
||||
INPUT_FLANGE_BOTTOM_Z,
|
||||
-1.0,
|
||||
),
|
||||
flip=True,
|
||||
),
|
||||
)
|
||||
input_part = scad.add_connector_rpart(
|
||||
input_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"lower_eccentric_axis",
|
||||
_axis_face(
|
||||
"input_lower_eccentric_axis",
|
||||
input_disk,
|
||||
LOWER_ECCENTRIC_CENTER,
|
||||
LOWER_CYCLOID_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
input_part = scad.add_connector_rpart(
|
||||
input_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"upper_eccentric_axis",
|
||||
_axis_face(
|
||||
"input_upper_eccentric_axis",
|
||||
input_disk,
|
||||
UPPER_ECCENTRIC_CENTER,
|
||||
UPPER_CYCLOID_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
lower_cycloid_part = scad.make_part_rpart(
|
||||
part_id="lower_ten_lobe_cycloidal_disc",
|
||||
body=lower_cycloidal_disc,
|
||||
name="Lower ten-lobe cycloidal disc",
|
||||
)
|
||||
lower_cycloid_part = scad.assign_material_rpart(
|
||||
lower_cycloid_part, bronze_material
|
||||
)
|
||||
lower_cycloid_part = scad.add_connector_rpart(
|
||||
lower_cycloid_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"eccentric_axis",
|
||||
_axis_face(
|
||||
"lower_cycloid_eccentric_axis",
|
||||
lower_cycloidal_disc,
|
||||
(0.0, 0.0),
|
||||
LOWER_CYCLOID_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
upper_cycloid_part = scad.make_part_rpart(
|
||||
part_id="upper_ten_lobe_cycloidal_disc",
|
||||
body=upper_cycloidal_disc,
|
||||
name="Upper ten-lobe cycloidal disc, 180 degree phased",
|
||||
)
|
||||
upper_cycloid_part = scad.assign_material_rpart(
|
||||
upper_cycloid_part, bronze_material
|
||||
)
|
||||
upper_cycloid_part = scad.add_connector_rpart(
|
||||
upper_cycloid_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"eccentric_axis",
|
||||
_axis_face(
|
||||
"upper_cycloid_eccentric_axis",
|
||||
upper_cycloidal_disc,
|
||||
(0.0, 0.0),
|
||||
UPPER_CYCLOID_CONNECTOR_Z,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
output_part = scad.make_part_rpart(
|
||||
part_id="output_three_thread_disk",
|
||||
body=output_disk,
|
||||
name="Output three threaded-hole disk with drive pins",
|
||||
)
|
||||
output_part = scad.assign_material_rpart(output_part, steel_material)
|
||||
output_part = scad.add_connector_rpart(
|
||||
output_part,
|
||||
scad.make_face_connector_rconnector(
|
||||
"axis",
|
||||
_axis_face(
|
||||
"output_axis",
|
||||
output_disk,
|
||||
(0.0, 0.0),
|
||||
PACKAGE_HEIGHT,
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
reducer = scad.make_assembly_rassembly(
|
||||
assembly_id="cycloidal_reducer_50mm_10x",
|
||||
name="50 mm diameter 10:1 cycloidal reducer",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=housing_part,
|
||||
component_id="fixed_housing",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Grounded fixed pin-ring housing",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=input_part,
|
||||
component_id="input_disk",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Input three-thread-hole disk",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=lower_cycloid_part,
|
||||
component_id="lower_cycloidal_disc",
|
||||
placement=_z_rotation_placement((ECCENTRICITY, 0.0, 0.0), 0.0),
|
||||
name="Lower cycloidal disc riding on 0 degree eccentric cam",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=upper_cycloid_part,
|
||||
component_id="upper_cycloidal_disc",
|
||||
placement=_z_rotation_placement((-ECCENTRICITY, 0.0, 0.0), 0.0),
|
||||
name="Upper cycloidal disc riding on 180 degree eccentric cam",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=output_part,
|
||||
component_id="output_disk",
|
||||
placement=_z_rotation_placement((0.0, 0.0, 0.0), 0.0),
|
||||
name="Output three-thread-hole disk",
|
||||
)
|
||||
|
||||
reducer = scad.ground_component_rassembly(reducer, "fixed_housing")
|
||||
reducer = scad.add_revolute_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="input_revolute",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="fixed_housing", connector_id="input_axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="input_disk", connector_id="axis"
|
||||
),
|
||||
name="Input disk rotates in the fixed housing",
|
||||
)
|
||||
reducer = scad.add_revolute_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="output_revolute",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="fixed_housing", connector_id="output_axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="output_disk", connector_id="axis"
|
||||
),
|
||||
name="Output disk rotates coaxially in the fixed housing",
|
||||
)
|
||||
reducer = scad.add_revolute_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="lower_cycloid_on_eccentric_cam",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="input_disk", connector_id="lower_eccentric_axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="lower_cycloidal_disc", connector_id="eccentric_axis"
|
||||
),
|
||||
name="Lower cycloidal disc rotates on the 0 degree input eccentric cam",
|
||||
)
|
||||
reducer = scad.add_revolute_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="upper_cycloid_on_eccentric_cam",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="input_disk", connector_id="upper_eccentric_axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="upper_cycloidal_disc", connector_id="eccentric_axis"
|
||||
),
|
||||
name="Upper cycloidal disc rotates on the 180 degree input eccentric cam",
|
||||
)
|
||||
reducer = scad.add_gear_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="fixed_pin_ring_to_lower_cycloid_spin",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="input_disk", connector_id="axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="lower_cycloidal_disc", connector_id="eccentric_axis"
|
||||
),
|
||||
pitch_radius_a=float(PIN_COUNT),
|
||||
pitch_radius_b=float(CYCLOID_LOBES),
|
||||
name="Fixed pin ring drives the lower cycloidal disc relative spin",
|
||||
)
|
||||
reducer = scad.add_gear_constraint_rassembly(
|
||||
assembly=reducer,
|
||||
constraint_id="fixed_pin_ring_to_upper_cycloid_spin",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="input_disk", connector_id="axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="upper_cycloidal_disc", connector_id="eccentric_axis"
|
||||
),
|
||||
pitch_radius_a=float(PIN_COUNT),
|
||||
pitch_radius_b=float(CYCLOID_LOBES),
|
||||
name="Fixed pin ring drives the upper cycloidal disc relative spin",
|
||||
)
|
||||
print(
|
||||
"assembly_plan: "
|
||||
f"diameter={PACKAGE_DIAMETER:.1f} height={PACKAGE_HEIGHT:.1f} "
|
||||
f"pins={PIN_COUNT} lobes={CYCLOID_LOBES} reduction={REDUCTION_RATIO}:1 "
|
||||
f"eccentricity={ECCENTRICITY:.2f} cycloid_discs=2 "
|
||||
f"eccentric_phase_degrees=0,180 "
|
||||
f"tooth_index_phase_degrees=0,{UPPER_CYCLOID_BODY_PHASE_DEGREES:.1f} "
|
||||
f"stack_height={CYCLOID_STACK_HEIGHT:.2f}"
|
||||
)
|
||||
print(
|
||||
"load_balance: "
|
||||
"lower_eccentric=(+e,0) upper_eccentric=(-e,0) "
|
||||
"output_pins_pass_through_both_discs contact_not_solved"
|
||||
)
|
||||
print(
|
||||
"kinematic_relation: "
|
||||
f"each_cycloid_relative=-{PIN_COUNT}/{CYCLOID_LOBES}*input "
|
||||
f"each_cycloid_global=output=-1/{REDUCTION_RATIO}*input via output pin holes"
|
||||
)
|
||||
|
||||
reducer = scad.solve_assembly_constraints_rassembly(reducer)
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(reducer)
|
||||
print(
|
||||
"assembly: "
|
||||
f"components={','.join(reducer.component_ids())} "
|
||||
f"grounded={','.join(reducer.grounded_component_ids)} "
|
||||
f"solved={report.solved} constraints={len(reducer.constraints)}"
|
||||
)
|
||||
for residual in report.residuals:
|
||||
print(
|
||||
f"constraint_{residual.constraint_id}: "
|
||||
f"translation={residual.translation_error:.6g} "
|
||||
f"angle={residual.angular_error_degrees:.6g} "
|
||||
f"ok={residual.within_tolerance}"
|
||||
)
|
||||
|
||||
preview = scad.make_compound_from_assembly_rcompound(reducer)
|
||||
_ground_compound("assembly_preview", preview)
|
||||
model_json = scad.export_model_json(session)
|
||||
|
||||
return reducer, preview, model_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
model_path = OUTPUT_DIR / "cycloidal_reducer_50mm_10x.model.json"
|
||||
step_path = OUTPUT_DIR / "cycloidal_reducer_50mm_10x.step"
|
||||
fcstd_path = OUTPUT_DIR / "cycloidal_reducer_50mm_10x.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json = build_cycloidal_reducer()
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(preview, str(step_path))
|
||||
|
||||
fcstd_status = "not attempted"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(model_json, str(fcstd_path.resolve()))
|
||||
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"failed ({exc.__class__.__name__}: {exc})"
|
||||
|
||||
payload = json.loads(model_json)
|
||||
replayed = scad.replay_model_json(model_json)
|
||||
solids = ql.select(preview.get_solids()).all()
|
||||
face_count = sum(len(ql.select(solid.get_faces()).all()) for solid in solids)
|
||||
volumes = [solid.get_volume() for solid in solids]
|
||||
|
||||
print(
|
||||
"package: "
|
||||
f"diameter={PACKAGE_DIAMETER:.1f} height={PACKAGE_HEIGHT:.1f} "
|
||||
f"outer_radius={PACKAGE_RADIUS:.1f}"
|
||||
)
|
||||
print(
|
||||
"mounting: "
|
||||
f"input=3xM2.5_envelope output=3xM2.5_envelope "
|
||||
f"hole_pcd={2.0 * MOUNT_HOLE_PITCH_RADIUS:.1f}"
|
||||
)
|
||||
print(f"assembly={assembly.assembly_id}")
|
||||
print("components=" + ",".join(assembly.component_ids()))
|
||||
print(f"preview_solids={len(solids)}")
|
||||
print(f"preview_faces={face_count}")
|
||||
print("volumes=" + ",".join(f"{volume:.1f}" for volume in volumes))
|
||||
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"step={step_path}")
|
||||
print(f"fcstd={fcstd_status}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Example 14: parameterized ball bearing standard assembly.
|
||||
|
||||
This example builds a small radial ball bearing through
|
||||
``scad.std.bearing.make_ball_bearing_rassembly`` and then keeps working inside
|
||||
that assembly by binding a demo shaft to the inner ring and a demo housing to
|
||||
the outer ring. The important bearing semantics are product-level, not just
|
||||
geometry: stable component ids expose the rings and balls, and the inner and
|
||||
outer rings are connected by a revolute constraint.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
OUT_DIR = Path("examples/out/ball_bearing_608_demo")
|
||||
|
||||
BORE_DIAMETER = 8.0
|
||||
OUTER_DIAMETER = 22.0
|
||||
BEARING_WIDTH = 5.0
|
||||
BALL_DIAMETER = 3.0
|
||||
BALL_COUNT = 7
|
||||
RACEWAY_CLEARANCE = 0.05
|
||||
EDGE_CHAMFER = 0.08
|
||||
INNER_RING_ANGLE = 35.0
|
||||
|
||||
|
||||
def _axis_part(part_id: str, solid: scad.Solid, name: str) -> scad.Part:
|
||||
part = scad.make_part_rpart(part_id, solid, name=name)
|
||||
top_face = max(
|
||||
solid.get_faces(),
|
||||
key=lambda face: face.get_center().z if face.get_normal_at().z > 0.7 else -999.0,
|
||||
)
|
||||
axis = scad.make_face_connector_rconnector("axis", top_face)
|
||||
return scad.add_connector_rpart(part, axis)
|
||||
|
||||
|
||||
def _make_demo_shaft() -> scad.Part:
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=BORE_DIAMETER / 2.0 - 0.2,
|
||||
height=BEARING_WIDTH + 4.0,
|
||||
bottom_face_center=(0.0, 0.0, -BEARING_WIDTH / 2.0 - 4.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
shaft = scad.apply_tag(shaft, "role.demo_shaft")
|
||||
return _axis_part("demo_shaft", shaft, "Demo shaft bound to inner ring")
|
||||
|
||||
|
||||
def _make_demo_housing() -> scad.Part:
|
||||
housing_outer = scad.make_cylinder_rsolid(
|
||||
radius=OUTER_DIAMETER / 2.0 + 3.0,
|
||||
height=BEARING_WIDTH + 0.75,
|
||||
bottom_face_center=(0.0, 0.0, -BEARING_WIDTH / 2.0 - 0.75),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bearing_pocket = scad.make_cylinder_rsolid(
|
||||
radius=OUTER_DIAMETER / 2.0 + 0.25,
|
||||
height=BEARING_WIDTH + 3.5,
|
||||
bottom_face_center=(0.0, 0.0, -BEARING_WIDTH / 2.0 - 1.75),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
housing = scad.cut_rsolid(
|
||||
housing_outer,
|
||||
bearing_pocket,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
housing = scad.apply_tag(housing, "role.demo_housing")
|
||||
return _axis_part("demo_housing", housing, "Demo housing bound to outer ring")
|
||||
|
||||
|
||||
def build_ball_bearing_demo():
|
||||
with scad.GraphSession() as session:
|
||||
bearing = scad.std.bearing.make_ball_bearing_rassembly(
|
||||
BORE_DIAMETER,
|
||||
OUTER_DIAMETER,
|
||||
BEARING_WIDTH,
|
||||
BALL_DIAMETER,
|
||||
BALL_COUNT,
|
||||
RACEWAY_CLEARANCE,
|
||||
EDGE_CHAMFER,
|
||||
"ball_bearing_608_demo",
|
||||
INNER_RING_ANGLE,
|
||||
)
|
||||
meta = bearing.get_metadata("std.bearing.ball_bearing")
|
||||
|
||||
outer_ring = bearing.get_component("outer_ring").item.body
|
||||
inner_ring = bearing.get_component("inner_ring").item.body
|
||||
print(
|
||||
"bearing_core",
|
||||
f"components={len(bearing.component_ids())}",
|
||||
f"balls={meta['ball_count']}",
|
||||
f"constraint={meta['revolute_constraint_id']}",
|
||||
)
|
||||
print(
|
||||
"ring_geometry",
|
||||
f"outer_faces={len(ql.faces().resolve(outer_ring))}",
|
||||
f"inner_faces={len(ql.faces().resolve(inner_ring))}",
|
||||
f"outer_volume={outer_ring.get_volume():.2f}",
|
||||
f"inner_volume={inner_ring.get_volume():.2f}",
|
||||
)
|
||||
|
||||
bearing = scad.add_component_rassembly(
|
||||
bearing,
|
||||
_make_demo_shaft(),
|
||||
component_id="demo_shaft",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
bearing = scad.add_component_rassembly(
|
||||
bearing,
|
||||
_make_demo_housing(),
|
||||
component_id="demo_housing",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
bearing = scad.add_fixed_constraint_rassembly(
|
||||
bearing,
|
||||
"shaft_to_inner_ring",
|
||||
scad.make_connector_ref_rconnectorref("inner_ring", "axis"),
|
||||
scad.make_connector_ref_rconnectorref("demo_shaft", "axis"),
|
||||
)
|
||||
bearing = scad.add_fixed_constraint_rassembly(
|
||||
bearing,
|
||||
"housing_to_outer_ring",
|
||||
scad.make_connector_ref_rconnectorref("outer_ring", "axis"),
|
||||
scad.make_connector_ref_rconnectorref("demo_housing", "axis"),
|
||||
)
|
||||
bearing = scad.solve_assembly_constraints_rassembly(bearing)
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(bearing)
|
||||
preview = scad.make_compound_from_assembly_rcompound(bearing)
|
||||
model_json = scad.export_model_json(session)
|
||||
|
||||
return bearing, report, preview, model_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
assembly, report, preview, model_json = build_ball_bearing_demo()
|
||||
|
||||
model_path = OUT_DIR / "ball_bearing_608_demo.model.json"
|
||||
step_path = OUT_DIR / "ball_bearing_608_demo.step"
|
||||
fcstd_path = OUT_DIR / "ball_bearing_608_demo.FCStd"
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
scad.export_step(preview, str(step_path))
|
||||
|
||||
fcstd_status = str(fcstd_path)
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(model_json, str(fcstd_path.resolve()))
|
||||
except Exception as exc: # pragma: no cover - depends on local FreeCAD install
|
||||
fcstd_status = f"skipped ({exc.__class__.__name__})"
|
||||
|
||||
payload = json.loads(model_json)
|
||||
print("assembly", assembly.assembly_id)
|
||||
print("components", assembly.component_ids())
|
||||
print("constraints", assembly.constraint_ids())
|
||||
print("solved", report.solved)
|
||||
print("preview_solids", len(preview.get_solids()))
|
||||
print("preview_volume", round(preview.get_volume(), 2))
|
||||
print("graph_nodes", len(payload["graph"]["nodes"]))
|
||||
print("wrote", model_path)
|
||||
print("wrote", step_path)
|
||||
print("fcstd", fcstd_status)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Example 15: export an internal cached mesh as OBJ.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/15_cached_mesh_obj_export.py
|
||||
|
||||
This is a developer-facing example for the mesh-cache groundwork. It
|
||||
intentionally does not call the public STL exporter. Instead it builds a normal
|
||||
SimpleCAD solid, reads the framework's internal cached mesh, and writes a common
|
||||
Wavefront OBJ mesh file from that pure triangle data.
|
||||
|
||||
Application code should not depend on ``simplecadapi._mesh``. Future structural
|
||||
checking APIs will consume the same internal mesh cache without exposing mesh
|
||||
extraction to framework users.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
import simplecadapi._mesh as internal_mesh
|
||||
|
||||
|
||||
OUT_DIR = Path("examples/out/cached_mesh_obj_export")
|
||||
|
||||
|
||||
def build_demo_solid() -> scad.Solid:
|
||||
"""Build a small bracket-like solid using only normal modeling APIs."""
|
||||
|
||||
base = scad.make_box_rsolid(
|
||||
width=34.0,
|
||||
height=20.0,
|
||||
depth=6.0,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
)
|
||||
through_hole = scad.make_cylinder_rsolid(
|
||||
radius=4.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(0.0, 0.0, -3.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
mount_slot = scad.make_box_rsolid(
|
||||
width=8.0,
|
||||
height=24.0,
|
||||
depth=10.0,
|
||||
bottom_face_center=(10.0, 0.0, -2.0),
|
||||
)
|
||||
boss = scad.make_cylinder_rsolid(
|
||||
radius=7.0,
|
||||
height=5.0,
|
||||
bottom_face_center=(-10.0, 0.0, 6.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
bracket = scad.cut_rsolid(
|
||||
base,
|
||||
through_hole,
|
||||
mount_slot,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
bracket = scad.union_rsolid([bracket, boss])
|
||||
return scad.apply_tag(shape=bracket, tag="role.cached_mesh_obj_demo")
|
||||
|
||||
|
||||
def write_cached_mesh_obj(solid: scad.Solid, path: Path) -> internal_mesh.TriMesh:
|
||||
"""Write a Solid's internal cached mesh to Wavefront OBJ."""
|
||||
|
||||
mesh = internal_mesh.cached_mesh(solid)
|
||||
if mesh is None:
|
||||
detail = internal_mesh.mesh_error(solid) or "no internal mesh cache"
|
||||
raise RuntimeError(f"Solid has no cached mesh: {detail}")
|
||||
|
||||
lines = [
|
||||
"# OBJ written from SimpleCAD internal cached mesh",
|
||||
"# This example intentionally bypasses scad.export_stl(...).",
|
||||
]
|
||||
for x, y, z in mesh.vertices:
|
||||
lines.append(f"v {x:.9g} {y:.9g} {z:.9g}")
|
||||
for a, b, c in mesh.triangles:
|
||||
lines.append(f"f {int(a) + 1} {int(b) + 1} {int(c) + 1}")
|
||||
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return mesh
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
solid = build_demo_solid()
|
||||
obj_path = OUT_DIR / "cached_mesh_bracket.obj"
|
||||
mesh = write_cached_mesh_obj(solid=solid, path=obj_path)
|
||||
lower, upper = mesh.bounds
|
||||
|
||||
print("volume", round(solid.get_volume(), 3))
|
||||
print("faces", len(solid.get_faces()))
|
||||
print("mesh", f"vertices={mesh.vertex_count}", f"triangles={mesh.triangle_count}")
|
||||
print("bounds", f"min={tuple(round(v, 3) for v in lower)}", f"max={tuple(round(v, 3) for v in upper)}")
|
||||
print("wrote_obj", obj_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
# Two-Stage Herringbone Planetary Reducer Design
|
||||
|
||||
## Part Analysis
|
||||
|
||||
- Product: compact coaxial two-stage planetary reducer with input and output flanges.
|
||||
- Envelope target: maximum outside diameter `50 mm`, total height `30 mm` along the reducer axis.
|
||||
- Power path: input flange -> input shaft -> stage 1 sun -> stage 1 carrier/intermediate shaft -> stage 2 sun -> stage 2 carrier/output shaft -> output flange.
|
||||
- Gear type: all active mesh gears are herringbone gears to cancel axial thrust and keep the stacked reducer compact.
|
||||
- Standard parts: herringbone gears, herringbone ring gears, and radial ball bearings come from `simplecadapi.std.gear` and `simplecadapi.std.bearing`.
|
||||
- Custom solids: housing sleeve, shafts, carriers, and flanges are integrated SimpleCAD solids built from cylinders, boxes, booleans, and tags.
|
||||
|
||||
## Structure Analysis
|
||||
|
||||
- Stage 1 uses `S1=12`, `P1=18`, `R1=48` teeth, giving fixed-ring planetary reduction `1 + R1 / S1 = 5:1`.
|
||||
- Stage 2 uses `S2=12`, `P2=12`, `R2=36` teeth, giving fixed-ring planetary reduction `1 + R2 / S2 = 4:1`.
|
||||
- Total reduction is `5 * 4 = 20:1`.
|
||||
- Module is `0.75 mm`; stage 1 ring pitch diameter is `36.0 mm` and stage 2 ring pitch diameter is `27.0 mm`.
|
||||
- Ring rim thickness is `1.90 mm`, keeping the ring outside radii below the `25 mm` product radius limit.
|
||||
- Gear stack heights are `4.6 mm` per stage, with carrier plates above each gear plane and flanges at the two axial ends.
|
||||
- The housing is a 50 mm OD sleeve with a large axial clearance bore and connector faces for all coaxial constraints.
|
||||
|
||||
## Bearing Analysis
|
||||
|
||||
- Input shaft bearing: one radial ball bearing near the input flange.
|
||||
- Intermediate shaft bearing: one radial ball bearing between stages to locate the stage 1 carrier / stage 2 sun drive shaft.
|
||||
- Output shaft bearing: one radial ball bearing near the output flange.
|
||||
- Stage 1 planet bearings: three radial ball bearing placements centered inside the stage 1 planet gears.
|
||||
- Stage 2 planet bearings: three radial ball bearing placements centered inside the stage 2 planet gears.
|
||||
- A single reusable `3.2 x 6.6 x 2.0 mm` bearing standard assembly is instanced at all nine friction locations. This keeps the graph replay stable while still using the standard bearing library.
|
||||
- Bearing assemblies are only placed for location and packaging. Their internal standard-library revolute detail remains visual; no extra reducer-level bearing rotation constraints are added.
|
||||
|
||||
## Assembly Plan
|
||||
|
||||
- Ground the outer housing and fix both internal ring gears to the housing axis.
|
||||
- Add revolute constraints for the input shaft, stage 1 carrier, stage 2 carrier, and all six planet axes.
|
||||
- Fix the stage 1 sun to the input shaft and input flange.
|
||||
- Fix the stage 2 sun to the stage 1 carrier intermediate shaft.
|
||||
- Fix the output flange to the stage 2 carrier/output shaft.
|
||||
- Add external gear constraints from each sun to its planets using `add_gear_constraint_rassembly`.
|
||||
- Add internal ring-to-planet mesh constraints using same-direction `add_belt_constraint_rassembly` with ring and planet pitch radii.
|
||||
- Use `GraphSession`, `export_session_json`, `export_model_json`, `import_model_json`, and `replay_model_json` in the build script for replayable output.
|
||||
- Ground every build step with concise QL-backed prints: part face counts, volumes, tags, bearing component counts, gear radii, constraint residuals, replay counts, and exported file paths.
|
||||
|
||||
## Validation Assumptions
|
||||
|
||||
- The envelope check uses analytical constants: outside diameter `50 mm`, axial span `30 mm`.
|
||||
- Tooth phasing is visual: each planet instance gets an angular placement so a tooth space is roughly aimed at the sun contact line.
|
||||
- Gear kinematics are represented by assembly constraints; the static CAD preview remains a positioned assembly, not a dynamic simulation.
|
||||
- If a boolean union needs one merged part, carrier and shaft cylinders overlap their plates and pads rather than merely touching.
|
||||
@@ -0,0 +1,419 @@
|
||||
"""Top-level compact two-stage planetary reducer assembly."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from bearings import (
|
||||
make_coaxial_bearing_rplacement,
|
||||
make_planet_bearing_rplacements,
|
||||
make_radial_ball_bearing_rassembly,
|
||||
)
|
||||
from carriers import make_stage_carrier_rpart
|
||||
from dimensions import (
|
||||
INPUT_BEARING_Z,
|
||||
INTERMEDIATE_BEARING_Z,
|
||||
OUTPUT_BEARING_Z,
|
||||
PLANET_COUNT,
|
||||
STAGE1_PLANET_BEARING,
|
||||
STAGE2_PLANET_BEARING,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
TOTAL_REDUCTION,
|
||||
StageSpec,
|
||||
UNIVERSAL_RADIAL_BEARING,
|
||||
)
|
||||
from flanges import make_input_flange_rpart, make_output_flange_rpart
|
||||
from gears import (
|
||||
make_planet_component_rplacement,
|
||||
make_stage_planet_gear_rpart,
|
||||
make_stage_ring_gear_rpart,
|
||||
make_stage_sun_gear_rpart,
|
||||
)
|
||||
from housing import make_reducer_housing_rpart
|
||||
from materials import make_reducer_materials_rdict
|
||||
from shafts import make_input_shaft_rpart
|
||||
|
||||
|
||||
def make_two_stage_planetary_reducer_rassembly() -> scad.Assembly:
|
||||
"""Build the full 20:1 compact reducer assembly and solve constraints."""
|
||||
|
||||
print(
|
||||
f"ratio_plan: stage1={STAGE_1.fixed_ring_ratio:.1f}:1 "
|
||||
f"stage2={STAGE_2.fixed_ring_ratio:.1f}:1 total={TOTAL_REDUCTION:.1f}:1"
|
||||
)
|
||||
materials = make_reducer_materials_rdict()
|
||||
|
||||
housing = make_reducer_housing_rpart(material=materials["housing"])
|
||||
input_flange = make_input_flange_rpart(material=materials["shaft"])
|
||||
output_flange = make_output_flange_rpart(material=materials["shaft"])
|
||||
input_shaft = make_input_shaft_rpart(material=materials["shaft"])
|
||||
|
||||
stage1_ring = make_stage_ring_gear_rpart(stage=STAGE_1, material=materials["gear"])
|
||||
stage1_sun = make_stage_sun_gear_rpart(
|
||||
stage=STAGE_1,
|
||||
bore_radius=1.56,
|
||||
material=materials["gear"],
|
||||
)
|
||||
stage1_planet = make_stage_planet_gear_rpart(
|
||||
stage=STAGE_1,
|
||||
bearing=STAGE1_PLANET_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
stage1_carrier = make_stage_carrier_rpart(
|
||||
stage=STAGE_1,
|
||||
material=materials["carrier"],
|
||||
)
|
||||
|
||||
stage2_ring = make_stage_ring_gear_rpart(stage=STAGE_2, material=materials["gear"])
|
||||
stage2_sun = make_stage_sun_gear_rpart(
|
||||
stage=STAGE_2,
|
||||
bore_radius=1.43,
|
||||
material=materials["gear"],
|
||||
)
|
||||
stage2_planet = make_stage_planet_gear_rpart(
|
||||
stage=STAGE_2,
|
||||
bearing=STAGE2_PLANET_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
stage2_carrier = make_stage_carrier_rpart(
|
||||
stage=STAGE_2,
|
||||
material=materials["carrier"],
|
||||
)
|
||||
|
||||
bearing = make_radial_ball_bearing_rassembly(
|
||||
bearing_id="micro_radial_ball_bearing",
|
||||
spec=UNIVERSAL_RADIAL_BEARING,
|
||||
)
|
||||
|
||||
reducer = scad.make_assembly_rassembly(
|
||||
assembly_id="compact_two_stage_planetary_reducer",
|
||||
name="58.8 mm OD 20:1 through-bolted herringbone planetary actuator reducer",
|
||||
)
|
||||
reducer = _add_fixed_components_rassembly(
|
||||
assembly=reducer,
|
||||
components=(
|
||||
("housing", housing, scad.identity_placement_rplacement(), "Fixed outer housing"),
|
||||
("input_flange", input_flange, scad.identity_placement_rplacement(), "Rotating input flange"),
|
||||
("output_flange", output_flange, scad.identity_placement_rplacement(), "Rotating output flange"),
|
||||
("input_shaft", input_shaft, scad.identity_placement_rplacement(), "Input shaft"),
|
||||
("stage1_carrier", stage1_carrier, scad.identity_placement_rplacement(), "Stage 1 carrier and stage 2 sun shaft"),
|
||||
("stage2_carrier", stage2_carrier, scad.identity_placement_rplacement(), "Stage 2 carrier and output shaft"),
|
||||
("stage1_ring", stage1_ring, _gear_stage_rplacement(stage=STAGE_1), "Stage 1 fixed ring"),
|
||||
("stage1_sun", stage1_sun, _gear_stage_rplacement(stage=STAGE_1), "Stage 1 sun"),
|
||||
("stage2_ring", stage2_ring, _gear_stage_rplacement(stage=STAGE_2), "Stage 2 fixed ring"),
|
||||
("stage2_sun", stage2_sun, _gear_stage_rplacement(stage=STAGE_2), "Stage 2 sun"),
|
||||
),
|
||||
)
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=stage1_planet,
|
||||
component_id=f"stage1_planet_{index + 1}",
|
||||
placement=make_planet_component_rplacement(stage=STAGE_1, planet_index=index),
|
||||
name=f"Stage 1 planet gear {index + 1}",
|
||||
)
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=stage2_planet,
|
||||
component_id=f"stage2_planet_{index + 1}",
|
||||
placement=make_planet_component_rplacement(stage=STAGE_2, planet_index=index),
|
||||
name=f"Stage 2 planet gear {index + 1}",
|
||||
)
|
||||
|
||||
reducer = _add_bearing_components_rassembly(
|
||||
assembly=reducer,
|
||||
bearing=bearing,
|
||||
)
|
||||
reducer = _add_public_interface_connectors_rassembly(assembly=reducer)
|
||||
reducer = _add_reducer_constraints_rassembly(assembly=reducer)
|
||||
reducer = scad.solve_assembly_constraints_rassembly(assembly=reducer, strict=True)
|
||||
_ground_constraint_report(assembly=reducer)
|
||||
return reducer
|
||||
|
||||
|
||||
def _add_fixed_components_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
components: tuple[tuple[str, scad.Part, scad.Placement, str], ...],
|
||||
) -> scad.Assembly:
|
||||
for component_id, item, placement, name in components:
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=name,
|
||||
)
|
||||
print(f"base_components: count={len(components)}")
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_bearing_components_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
bearing: scad.Assembly,
|
||||
) -> scad.Assembly:
|
||||
bearing_component_count = 0
|
||||
for component_id, bearing, placement, name in (
|
||||
(
|
||||
"input_bearing",
|
||||
bearing,
|
||||
make_coaxial_bearing_rplacement(z=INPUT_BEARING_Z),
|
||||
"Input shaft radial ball bearing",
|
||||
),
|
||||
(
|
||||
"intermediate_bearing",
|
||||
bearing,
|
||||
make_coaxial_bearing_rplacement(z=INTERMEDIATE_BEARING_Z),
|
||||
"Intermediate shaft radial ball bearing",
|
||||
),
|
||||
(
|
||||
"output_bearing",
|
||||
bearing,
|
||||
make_coaxial_bearing_rplacement(z=OUTPUT_BEARING_Z),
|
||||
"Output shaft radial ball bearing",
|
||||
),
|
||||
):
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=bearing,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=name,
|
||||
)
|
||||
bearing_component_count += 1
|
||||
|
||||
for index, placement in enumerate(make_planet_bearing_rplacements(stage=STAGE_1)):
|
||||
component_id = f"stage1_planet_bearing_{index + 1}"
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=bearing,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=f"Stage 1 planet {index + 1} ball bearing",
|
||||
)
|
||||
bearing_component_count += 1
|
||||
|
||||
for index, placement in enumerate(make_planet_bearing_rplacements(stage=STAGE_2)):
|
||||
component_id = f"stage2_planet_bearing_{index + 1}"
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=bearing,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=f"Stage 2 planet {index + 1} ball bearing",
|
||||
)
|
||||
bearing_component_count += 1
|
||||
|
||||
print(f"bearing_components: count={bearing_component_count} grounded=0")
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_public_interface_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
"""Expose stable actuator module datums without leaking private component ids."""
|
||||
|
||||
forwarded = (
|
||||
("housing_mount_axis", "housing", "output_axis", "Fixed case mounting datum"),
|
||||
("input_motor_axis", "input_flange", "axis", "Input flange datum for motor can"),
|
||||
("output_link_axis", "output_flange", "axis", "Output flange datum for driven link"),
|
||||
)
|
||||
for connector_id, source_component_id, source_connector_id, name in forwarded:
|
||||
assembly = scad.forward_connector_rassembly(
|
||||
assembly=assembly,
|
||||
connector_id=connector_id,
|
||||
source_component_id=source_component_id,
|
||||
source_connector_id=source_connector_id,
|
||||
name=name,
|
||||
)
|
||||
print("reducer_public_connectors: " + ",".join(connector_id for connector_id, *_ in forwarded))
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_reducer_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="housing")
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage1_ring")
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage2_ring")
|
||||
|
||||
fixed_pairs = (
|
||||
("stage1_ring_fixed", "housing", "stage1_axis", "stage1_ring", "axis"),
|
||||
("stage2_ring_fixed", "housing", "stage2_axis", "stage2_ring", "axis"),
|
||||
("input_flange_to_shaft", "input_flange", "axis", "input_shaft", "flange_axis"),
|
||||
("stage1_sun_to_input_shaft", "input_shaft", "sun_axis", "stage1_sun", "axis"),
|
||||
("stage2_sun_to_stage1_carrier", "stage1_carrier", "stage2_sun_axis", "stage2_sun", "axis"),
|
||||
("output_flange_to_stage2_carrier", "stage2_carrier", "output_axis", "output_flange", "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=_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=_ref(component_id=b_component, connector_id=b_connector),
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
revolutes = (
|
||||
("input_shaft_revolute", "housing", "input_axis", "input_shaft", "sun_axis"),
|
||||
("stage1_carrier_revolute", "housing", "stage2_axis", "stage1_carrier", "carrier_axis"),
|
||||
("stage2_carrier_revolute", "housing", "output_axis", "stage2_carrier", "carrier_axis"),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, b_component, b_connector in revolutes:
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=constraint_id,
|
||||
connector_a=_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=_ref(component_id=b_component, connector_id=b_connector),
|
||||
drive_angle_degrees=0.0,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
assembly = _add_stage_mesh_constraints_rassembly(
|
||||
assembly=assembly,
|
||||
stage=STAGE_1,
|
||||
driver_component_id="input_shaft",
|
||||
driver_connector_id="sun_axis",
|
||||
ring_component_id="stage1_ring",
|
||||
carrier_component_id="stage1_carrier",
|
||||
)
|
||||
assembly = _add_stage_mesh_constraints_rassembly(
|
||||
assembly=assembly,
|
||||
stage=STAGE_2,
|
||||
driver_component_id="stage1_carrier",
|
||||
driver_connector_id="carrier_axis",
|
||||
ring_component_id="stage2_ring",
|
||||
carrier_component_id="stage2_carrier",
|
||||
)
|
||||
assembly = _add_bearing_interface_constraints_rassembly(assembly=assembly)
|
||||
print("constraints_added: fixed=6 revolute=27 gear_mesh=6 internal_mesh=6")
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_bearing_interface_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
coaxial_interfaces = (
|
||||
("input_bearing_outer_to_housing", "housing", "input_bearing_axis", "input_bearing", "outer_axis"),
|
||||
("input_bearing_inner_to_shaft", "input_shaft", "input_bearing_axis", "input_bearing", "inner_axis"),
|
||||
("intermediate_bearing_outer_to_housing", "housing", "intermediate_bearing_axis", "intermediate_bearing", "outer_axis"),
|
||||
("intermediate_bearing_inner_to_stage1_carrier", "stage1_carrier", "intermediate_bearing_axis", "intermediate_bearing", "inner_axis"),
|
||||
("output_bearing_outer_to_housing", "housing", "output_bearing_axis", "output_bearing", "outer_axis"),
|
||||
("output_bearing_inner_to_stage2_carrier", "stage2_carrier", "output_bearing_axis", "output_bearing", "inner_axis"),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, bearing_component, bearing_connector in coaxial_interfaces:
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=constraint_id,
|
||||
connector_a=_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=_ref(component_id=bearing_component, connector_id=bearing_connector),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
for stage, carrier_component_id in ((STAGE_1, "stage1_carrier"), (STAGE_2, "stage2_carrier")):
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_id = f"{stage.stage_id}_planet_{index + 1}"
|
||||
bearing_id = f"{stage.stage_id}_planet_bearing_{index + 1}"
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{bearing_id}_outer_to_planet",
|
||||
connector_a=_ref(component_id=planet_id, connector_id="bearing_axis"),
|
||||
connector_b=_ref(component_id=bearing_id, connector_id="outer_axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{bearing_id} outer ring to planet gear bore",
|
||||
)
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{bearing_id}_inner_to_carrier_pin",
|
||||
connector_a=_ref(
|
||||
component_id=carrier_component_id,
|
||||
connector_id=f"planet_{index + 1}_bearing_axis",
|
||||
),
|
||||
connector_b=_ref(component_id=bearing_id, connector_id="inner_axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{bearing_id} inner ring to carrier pin",
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_stage_mesh_constraints_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
stage: StageSpec,
|
||||
driver_component_id: str,
|
||||
driver_connector_id: str,
|
||||
ring_component_id: str,
|
||||
carrier_component_id: str,
|
||||
) -> scad.Assembly:
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_component_id = f"{stage.stage_id}_planet_{index + 1}"
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{stage.stage_id}_planet_{index + 1}_revolute",
|
||||
connector_a=_ref(
|
||||
component_id=carrier_component_id,
|
||||
connector_id=f"planet_{index + 1}_axis",
|
||||
),
|
||||
connector_b=_ref(component_id=planet_component_id, connector_id="axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{stage.label} planet {index + 1} pin bearing axis",
|
||||
)
|
||||
assembly = scad.add_gear_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{stage.stage_id}_sun_planet_{index + 1}_external_mesh",
|
||||
connector_a=_ref(component_id=driver_component_id, connector_id=driver_connector_id),
|
||||
connector_b=_ref(component_id=planet_component_id, connector_id="axis"),
|
||||
pitch_radius_a=stage.sun_pitch_radius,
|
||||
pitch_radius_b=stage.planet_pitch_radius,
|
||||
phase_offset=None,
|
||||
name=f"{stage.label} external sun to planet {index + 1} mesh",
|
||||
)
|
||||
assembly = scad.add_belt_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{stage.stage_id}_ring_planet_{index + 1}_internal_mesh",
|
||||
connector_a=_ref(component_id=ring_component_id, connector_id="axis"),
|
||||
connector_b=_ref(component_id=planet_component_id, connector_id="axis"),
|
||||
pulley_radius_a=stage.ring_pitch_radius,
|
||||
pulley_radius_b=stage.planet_pitch_radius,
|
||||
phase_offset=None,
|
||||
name=f"{stage.label} internal fixed-ring to planet {index + 1} mesh",
|
||||
)
|
||||
print(
|
||||
f"{stage.stage_id}_constraints: sun_r={stage.sun_pitch_radius:.3f} "
|
||||
f"planet_r={stage.planet_pitch_radius:.3f} ring_r={stage.ring_pitch_radius:.3f}"
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _gear_stage_rplacement(*, stage: StageSpec) -> scad.Placement:
|
||||
return scad.make_placement_rplacement(
|
||||
origin=(0.0, 0.0, stage.bottom_z),
|
||||
x_axis=(1.0, 0.0, 0.0),
|
||||
y_axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
|
||||
|
||||
def _ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
return scad.make_connector_ref_rconnectorref(
|
||||
component_id=component_id,
|
||||
connector_id=connector_id,
|
||||
)
|
||||
|
||||
|
||||
def _ground_constraint_report(*, assembly: scad.Assembly) -> None:
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
|
||||
print(
|
||||
f"assembly_constraints: solved={report.solved} components={len(assembly.component_ids())} "
|
||||
f"constraints={len(assembly.constraints)} grounded={len(assembly.grounded_component_ids)}"
|
||||
)
|
||||
for residual in report.residuals:
|
||||
print(
|
||||
f"constraint_{residual.constraint_id}: translation={residual.translation_error:.6g} "
|
||||
f"angle={residual.angular_error_degrees:.6g} ok={residual.within_tolerance}"
|
||||
)
|
||||
STAGE1_PLANET_BEARING,
|
||||
STAGE2_PLANET_BEARING,
|
||||
UNIVERSAL_RADIAL_BEARING,
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Standard ball bearing assemblies placed in the reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import make_z_rotation_rplacement
|
||||
from dimensions import BearingSpec, PLANET_COUNT, StageSpec
|
||||
|
||||
|
||||
def make_radial_ball_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
spec: BearingSpec,
|
||||
) -> scad.Assembly:
|
||||
"""Create a standard radial ball bearing assembly for reducer placement."""
|
||||
|
||||
bearing = scad.std.bearing.make_ball_bearing_rassembly(
|
||||
bore_diameter=spec.bore_diameter,
|
||||
outer_diameter=spec.outer_diameter,
|
||||
bearing_width=spec.width,
|
||||
ball_diameter=spec.ball_diameter,
|
||||
ball_count=spec.ball_count,
|
||||
raceway_clearance=spec.raceway_clearance,
|
||||
edge_chamfer=spec.edge_chamfer,
|
||||
assembly_id=bearing_id,
|
||||
drive_angle_degrees=None,
|
||||
)
|
||||
meta = bearing.get_metadata("std.bearing.ball_bearing")
|
||||
outer_ring = bearing.get_component("outer_ring").item.body
|
||||
inner_ring = bearing.get_component("inner_ring").item.body
|
||||
print(
|
||||
f"bearing_{bearing_id}: components={len(bearing.component_ids())} balls={meta['ball_count']} "
|
||||
f"od={spec.outer_diameter:.2f} bore={spec.bore_diameter:.2f} width={spec.width:.2f}"
|
||||
)
|
||||
print(
|
||||
f"bearing_{bearing_id}_rings: outer_faces={len(outer_ring.get_faces())} "
|
||||
f"inner_faces={len(inner_ring.get_faces())}"
|
||||
)
|
||||
return bearing
|
||||
|
||||
|
||||
def make_coaxial_bearing_rplacement(*, z: float) -> scad.Placement:
|
||||
"""Return a coaxial bearing placement at the requested axial center."""
|
||||
|
||||
return make_z_rotation_rplacement(origin=(0.0, 0.0, z), angle_degrees=0.0)
|
||||
|
||||
|
||||
def make_planet_bearing_rplacements(*, stage: StageSpec) -> list[scad.Placement]:
|
||||
"""Return placed bearing placements centered in all planets of one stage."""
|
||||
|
||||
placements = []
|
||||
for index in range(PLANET_COUNT):
|
||||
angle = math.radians(360.0 * index / PLANET_COUNT)
|
||||
center = (
|
||||
stage.planet_center_radius * math.cos(angle),
|
||||
stage.planet_center_radius * math.sin(angle),
|
||||
stage.mid_z,
|
||||
)
|
||||
placements.append(make_z_rotation_rplacement(origin=center, angle_degrees=0.0))
|
||||
print(
|
||||
f"{stage.stage_id}_planet_bearing_{index + 1}: "
|
||||
f"center=({center[0]:.3f},{center[1]:.3f},{center[2]:.3f})"
|
||||
)
|
||||
return placements
|
||||
@@ -0,0 +1,251 @@
|
||||
"""Carrier plates, planet pins, and coaxial output shafts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import _apply_tags, add_placement_axis_connector_rpart, make_axis_part_rpart
|
||||
from dimensions import (
|
||||
INTERMEDIATE_BEARING_Z,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
OUTPUT_BEARING_Z,
|
||||
OUTPUT_SHAFT_RADIUS,
|
||||
PLANET_COUNT,
|
||||
STAGE1_ARM_WIDTH,
|
||||
STAGE1_CARRIER_PLATE_BOTTOM_Z,
|
||||
STAGE1_CARRIER_PLATE_THICKNESS,
|
||||
STAGE1_CARRIER_SHAFT_RADIUS,
|
||||
STAGE1_HUB_RADIUS,
|
||||
STAGE1_PAD_RADIUS,
|
||||
STAGE1_PIN_BOTTOM_Z,
|
||||
STAGE1_PIN_LAND_RADIUS,
|
||||
STAGE1_PIN_RADIUS,
|
||||
STAGE2_ARM_WIDTH,
|
||||
STAGE2_CARRIER_PLATE_BOTTOM_Z,
|
||||
STAGE2_CARRIER_PLATE_THICKNESS,
|
||||
STAGE2_HUB_RADIUS,
|
||||
STAGE2_PAD_RADIUS,
|
||||
STAGE2_PIN_BOTTOM_Z,
|
||||
STAGE2_PIN_LAND_RADIUS,
|
||||
STAGE2_PIN_RADIUS,
|
||||
STAGE_2,
|
||||
StageSpec,
|
||||
)
|
||||
|
||||
|
||||
def make_stage_carrier_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create one carrier with three planet pins and any coaxial drive shaft."""
|
||||
|
||||
if stage.stage_id == "stage1":
|
||||
solid = _make_carrier_solid_rsolid(
|
||||
stage=stage,
|
||||
plate_bottom_z=STAGE1_CARRIER_PLATE_BOTTOM_Z,
|
||||
plate_thickness=STAGE1_CARRIER_PLATE_THICKNESS,
|
||||
pin_bottom_z=STAGE1_PIN_BOTTOM_Z,
|
||||
pin_radius=STAGE1_PIN_RADIUS,
|
||||
pin_land_radius=STAGE1_PIN_LAND_RADIUS,
|
||||
hub_radius=STAGE1_HUB_RADIUS,
|
||||
arm_width=STAGE1_ARM_WIDTH,
|
||||
pad_radius=STAGE1_PAD_RADIUS,
|
||||
central_shaft_radius=STAGE1_CARRIER_SHAFT_RADIUS,
|
||||
central_shaft_top_z=STAGE_2.top_z,
|
||||
)
|
||||
connector_specs = [
|
||||
{
|
||||
"connector_id": "carrier_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": STAGE_2.top_z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
{
|
||||
"connector_id": "stage2_sun_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": STAGE_2.top_z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
]
|
||||
elif stage.stage_id == "stage2":
|
||||
solid = _make_carrier_solid_rsolid(
|
||||
stage=stage,
|
||||
plate_bottom_z=STAGE2_CARRIER_PLATE_BOTTOM_Z,
|
||||
plate_thickness=STAGE2_CARRIER_PLATE_THICKNESS,
|
||||
pin_bottom_z=STAGE2_PIN_BOTTOM_Z,
|
||||
pin_radius=STAGE2_PIN_RADIUS,
|
||||
pin_land_radius=STAGE2_PIN_LAND_RADIUS,
|
||||
hub_radius=STAGE2_HUB_RADIUS,
|
||||
arm_width=STAGE2_ARM_WIDTH,
|
||||
pad_radius=STAGE2_PAD_RADIUS,
|
||||
central_shaft_radius=OUTPUT_SHAFT_RADIUS,
|
||||
central_shaft_top_z=OUTPUT_FLANGE_TOP_Z,
|
||||
)
|
||||
connector_specs = [
|
||||
{
|
||||
"connector_id": "carrier_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": OUTPUT_FLANGE_TOP_Z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
{
|
||||
"connector_id": "output_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": OUTPUT_FLANGE_TOP_Z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"unsupported carrier stage: {stage.stage_id}")
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
connector_specs.append(
|
||||
{
|
||||
"connector_id": f"planet_{index + 1}_axis",
|
||||
"center_xy": _planet_center(stage=stage, planet_index=index),
|
||||
"target_z": stage.top_z,
|
||||
"normal_z": 1.0,
|
||||
}
|
||||
)
|
||||
|
||||
part = make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_carrier",
|
||||
solid=solid,
|
||||
name=f"{stage.label} carrier with planet pins",
|
||||
material=material,
|
||||
connector_specs=connector_specs,
|
||||
)
|
||||
if stage.stage_id == "stage1":
|
||||
part = add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id="intermediate_bearing_axis",
|
||||
origin=(0.0, 0.0, INTERMEDIATE_BEARING_Z),
|
||||
name="Intermediate bearing shaft seat axis",
|
||||
)
|
||||
else:
|
||||
part = add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id="output_bearing_axis",
|
||||
origin=(0.0, 0.0, OUTPUT_BEARING_Z),
|
||||
name="Output bearing shaft seat axis",
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
part = add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id=f"planet_{index + 1}_bearing_axis",
|
||||
origin=(*_planet_center(stage=stage, planet_index=index), stage.mid_z),
|
||||
name=f"{stage.label} planet {index + 1} bearing pin axis",
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
def _make_carrier_solid_rsolid(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
plate_bottom_z: float,
|
||||
plate_thickness: float,
|
||||
pin_bottom_z: float,
|
||||
pin_radius: float,
|
||||
pin_land_radius: float,
|
||||
hub_radius: float,
|
||||
arm_width: float,
|
||||
pad_radius: float,
|
||||
central_shaft_radius: float,
|
||||
central_shaft_top_z: float,
|
||||
) -> scad.Solid:
|
||||
hub = scad.make_cylinder_rsolid(
|
||||
radius=hub_radius,
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(0.0, 0.0, plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
solids = [hub]
|
||||
|
||||
shaft_bottom_z = plate_bottom_z - 0.05
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=central_shaft_radius,
|
||||
height=central_shaft_top_z - shaft_bottom_z,
|
||||
bottom_face_center=(0.0, 0.0, shaft_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
# Do not let the carrier arms merely kiss the hub at a shallow overlap.
|
||||
# The previous 0.35 mm embed was enough for OCC to return one Solid, but two
|
||||
# rotated arms could still read visually like separate fork prongs in export
|
||||
# views. A real carrier web should grow well into the center hub so torque is
|
||||
# carried through material, not through a tangent-looking boolean seam.
|
||||
arm_inner_radius = max(central_shaft_radius + 0.25, hub_radius - 1.25)
|
||||
arm_outer_radius = stage.planet_center_radius + pad_radius - 0.25
|
||||
arm_length = arm_outer_radius - arm_inner_radius
|
||||
arm_center_radius = (arm_inner_radius + arm_outer_radius) / 2.0
|
||||
pin_height = plate_bottom_z + plate_thickness - pin_bottom_z
|
||||
pin_land_height = stage.top_z - pin_bottom_z
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
carrier_angle = 360.0 * index / PLANET_COUNT
|
||||
center_xy = _planet_center(stage=stage, planet_index=index)
|
||||
|
||||
arm = scad.make_box_rsolid(
|
||||
width=arm_length,
|
||||
height=arm_width,
|
||||
depth=plate_thickness,
|
||||
bottom_face_center=(arm_center_radius, 0.0, plate_bottom_z),
|
||||
)
|
||||
if abs(carrier_angle) > 1.0e-9:
|
||||
arm = scad.rotate_shape(
|
||||
shape=arm,
|
||||
angle=carrier_angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
solids.append(arm)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=pad_radius,
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=pin_radius,
|
||||
height=pin_height,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=pin_land_radius,
|
||||
height=pin_land_height,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
carrier = scad.union_rsolid(solids, glue=False)
|
||||
carrier = _apply_tags(
|
||||
carrier,
|
||||
tags=(f"role.{stage.stage_id}.planet_carrier", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"{stage.stage_id}_carrier_geometry: center_radius={stage.planet_center_radius:.3f} "
|
||||
f"arm_length={arm_length:.3f} arm_hub_embed={hub_radius - arm_inner_radius:.3f} "
|
||||
f"pin_height={pin_height:.3f} shaft_top={central_shaft_top_z:.3f} "
|
||||
f"faces={len(carrier.get_faces())} volume={carrier.get_volume():.3f}"
|
||||
)
|
||||
return carrier
|
||||
|
||||
|
||||
def _planet_center(*, stage: StageSpec, planet_index: int) -> tuple[float, float]:
|
||||
angle = math.radians(360.0 * planet_index / PLANET_COUNT)
|
||||
return (
|
||||
stage.planet_center_radius * math.cos(angle),
|
||||
stage.planet_center_radius * math.sin(angle),
|
||||
)
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
"""Run static collision verification on the compact reducer example.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/16_compact_two_stage_planetary_reducer/collision_probe.py
|
||||
|
||||
This probe builds the solved reducer assembly without exporting STEP/FCStd, then
|
||||
checks the current pose with ``scad.verifier.check_collision_rcollisionreport``.
|
||||
The current verifier uses FCL-reported mesh contact penetration only; it does
|
||||
not handle complete containment cases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from assembly import make_two_stage_planetary_reducer_rassembly
|
||||
|
||||
|
||||
sys.setrecursionlimit(30000)
|
||||
|
||||
|
||||
def _build_reducer_quietly() -> tuple[scad.Assembly, int, float]:
|
||||
log_buffer = io.StringIO()
|
||||
start = time.perf_counter()
|
||||
with contextlib.redirect_stdout(log_buffer):
|
||||
with scad.GraphSession(graph_id="compact_reducer_collision_probe"):
|
||||
assembly = make_two_stage_planetary_reducer_rassembly()
|
||||
elapsed = time.perf_counter() - start
|
||||
return assembly, len(log_buffer.getvalue().splitlines()), elapsed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
assembly, log_lines, build_seconds = _build_reducer_quietly()
|
||||
|
||||
start = time.perf_counter()
|
||||
report = scad.verifier.check_collision_rcollisionreport(
|
||||
assembly=assembly,
|
||||
config=scad.verifier.CollisionCheckConfig(
|
||||
max_allowed_penetration=0.02,
|
||||
max_contacts_per_pair=16,
|
||||
),
|
||||
)
|
||||
check_seconds = time.perf_counter() - start
|
||||
|
||||
print("assembly", assembly.assembly_id)
|
||||
print("build_log_lines", log_lines)
|
||||
print("build_seconds", round(build_seconds, 3))
|
||||
print("check_seconds", round(check_seconds, 3))
|
||||
print("completed", report.completed)
|
||||
print("passed", report.passed)
|
||||
print("checked_pair_count", report.checked_pair_count)
|
||||
print("failed_pair_count", report.failed_pair_count)
|
||||
print("warning_count", len(report.warnings))
|
||||
|
||||
for warning in report.warnings[:20]:
|
||||
path = "/".join(warning.component_path or ())
|
||||
print("warning", warning.code, path, warning.message)
|
||||
|
||||
for failure in sorted(
|
||||
report.failures,
|
||||
key=lambda item: item.penetration_depth,
|
||||
reverse=True,
|
||||
)[:20]:
|
||||
print(
|
||||
"failure",
|
||||
"/".join(failure.component_a),
|
||||
"/".join(failure.component_b),
|
||||
"depth",
|
||||
round(failure.penetration_depth, 4),
|
||||
"contacts",
|
||||
len(failure.contacts),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Shared construction and grounding helpers for the reducer example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
def make_z_rotation_rplacement(
|
||||
*,
|
||||
origin: tuple[float, float, float],
|
||||
angle_degrees: float,
|
||||
) -> scad.Placement:
|
||||
"""Return a placement rotated about the local Z axis."""
|
||||
|
||||
angle_radians = math.radians(angle_degrees)
|
||||
cos_a = math.cos(angle_radians)
|
||||
sin_a = math.sin(angle_radians)
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(cos_a, sin_a, 0.0),
|
||||
y_axis=(-sin_a, cos_a, 0.0),
|
||||
)
|
||||
|
||||
|
||||
def make_annular_cylinder_rsolid(
|
||||
*,
|
||||
outer_radius: float,
|
||||
inner_radius: float,
|
||||
height: float,
|
||||
bottom_z: float,
|
||||
tag: str,
|
||||
) -> scad.Solid:
|
||||
"""Create a single hollow cylindrical solid with a through bore."""
|
||||
|
||||
if inner_radius <= 0.0 or outer_radius <= inner_radius:
|
||||
raise ValueError("annular cylinder requires 0 < inner_radius < outer_radius")
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=outer_radius,
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=inner_radius,
|
||||
height=height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
annular = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
annular = scad.apply_tag(shape=annular, tag=tag)
|
||||
_ground_solid(label=tag, solid=annular)
|
||||
return annular
|
||||
|
||||
|
||||
def make_axis_connector_rconnector(
|
||||
*,
|
||||
connector_id: str,
|
||||
solid: scad.Solid,
|
||||
center_xy: tuple[float, float],
|
||||
target_z: float,
|
||||
normal_z: float,
|
||||
name: str | None = None,
|
||||
flip: bool = False,
|
||||
) -> scad.Connector:
|
||||
"""Create a face connector on the axial face nearest the requested center."""
|
||||
|
||||
face = _axis_face(
|
||||
label=connector_id,
|
||||
solid=solid,
|
||||
center_xy=center_xy,
|
||||
target_z=target_z,
|
||||
normal_z=normal_z,
|
||||
)
|
||||
return scad.make_face_connector_rconnector(
|
||||
connector_id=connector_id,
|
||||
face=face,
|
||||
name=name,
|
||||
flip=flip,
|
||||
)
|
||||
|
||||
|
||||
def make_axis_part_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
solid: scad.Solid,
|
||||
name: str,
|
||||
connector_specs: Iterable[dict[str, object]],
|
||||
material: scad.Material | None = None,
|
||||
) -> scad.Part:
|
||||
"""Wrap a solid as a Part and attach axial face connectors."""
|
||||
|
||||
part = scad.make_part_rpart(part_id=part_id, body=solid, name=name)
|
||||
if material is not None:
|
||||
part = scad.assign_material_rpart(part=part, material=material)
|
||||
for spec in connector_specs:
|
||||
part = scad.add_connector_rpart(
|
||||
part=part,
|
||||
connector=make_axis_connector_rconnector(
|
||||
connector_id=str(spec["connector_id"]),
|
||||
solid=solid,
|
||||
center_xy=spec["center_xy"], # type: ignore[arg-type]
|
||||
target_z=float(spec["target_z"]),
|
||||
normal_z=float(spec["normal_z"]),
|
||||
name=spec.get("name"), # type: ignore[arg-type]
|
||||
flip=bool(spec.get("flip", False)),
|
||||
),
|
||||
)
|
||||
print(f"part_{part_id}: connectors={len(part.connectors)} material={bool(material)}")
|
||||
return part
|
||||
|
||||
|
||||
def add_placement_axis_connector_rpart(
|
||||
*,
|
||||
part: scad.Part,
|
||||
connector_id: str,
|
||||
origin: tuple[float, float, float],
|
||||
name: str | None = None,
|
||||
) -> scad.Part:
|
||||
"""Attach a topology-free axis connector at an explicit local placement."""
|
||||
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=connector_id,
|
||||
placement=scad.make_placement_rplacement(origin=origin),
|
||||
name=name,
|
||||
)
|
||||
return scad.add_connector_rpart(part=part, connector=connector)
|
||||
|
||||
|
||||
def _apply_tags(shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
|
||||
"""Apply normalized tags through the public SimpleCAD tag API."""
|
||||
|
||||
tagged = shape
|
||||
for tag in tags:
|
||||
tagged = scad.apply_tag(shape=tagged, tag=tag)
|
||||
return tagged
|
||||
|
||||
|
||||
def _axis_face(
|
||||
*,
|
||||
label: str,
|
||||
solid: scad.Solid,
|
||||
center_xy: tuple[float, float],
|
||||
target_z: float,
|
||||
normal_z: float,
|
||||
) -> scad.Face:
|
||||
candidates = []
|
||||
for face in ql.select(items=solid.get_faces()).all():
|
||||
normal = face.get_normal_at()
|
||||
if normal_z > 0.0 and normal.z < 0.65:
|
||||
continue
|
||||
if normal_z < 0.0 and normal.z > -0.65:
|
||||
continue
|
||||
center = face.get_center()
|
||||
xy_error = math.hypot(center.x - center_xy[0], center.y - center_xy[1])
|
||||
z_error = abs(center.z - target_z)
|
||||
candidates.append((z_error * 1000.0 + xy_error, face, center, normal))
|
||||
|
||||
if not candidates:
|
||||
raise ValueError(f"no axial connector face found for {label}")
|
||||
|
||||
_score, face, center, normal = min(candidates, key=lambda item: item[0])
|
||||
print(
|
||||
f"connector_{label}: center=({center.x:.3f},{center.y:.3f},{center.z:.3f}) "
|
||||
f"normal=({normal.x:.2f},{normal.y:.2f},{normal.z:.2f}) area={face.get_area():.3f}"
|
||||
)
|
||||
return face
|
||||
|
||||
|
||||
def _ground_solid(*, label: str, solid: scad.Solid) -> None:
|
||||
faces = ql.select(items=solid.get_faces()).all()
|
||||
role_faces = ql.select(items=faces).where(ql.tag(pattern="role.*")).all()
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
|
||||
|
||||
def _ground_compound(*, label: str, compound: scad.Compound) -> None:
|
||||
"""Print a compact QL-backed summary of an assembly preview compound."""
|
||||
|
||||
solids = ql.select(items=compound.get_solids()).all()
|
||||
face_count = sum(len(ql.select(items=solid.get_faces()).all()) for solid in solids)
|
||||
volume = sum(solid.get_volume() for solid in solids)
|
||||
print(f"{label}: solids={len(solids)} faces={face_count} volume={volume:.3f}")
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Design constants for the compact two-stage planetary reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
PLANET_COUNT = 3
|
||||
MODULE = 0.75
|
||||
PRESSURE_ANGLE = 20.0
|
||||
HELIX_ANGLE = 27.0
|
||||
GEAR_HEIGHT = 4.60
|
||||
ADDENDUM_FACTOR = 1.0
|
||||
CLEARANCE_FACTOR = 0.25
|
||||
RING_RIM_THICKNESS = 1.90
|
||||
FIXED_RING_HOUSING_SUPPORT_OVERLAP = 0.30
|
||||
BACKLASH = 0.02
|
||||
|
||||
HOUSING_OUTER_RADIUS = 29.4
|
||||
# The original 50 mm reducer envelope did not leave enough radial room for real
|
||||
# M3-class housing screws outside the ring gear. The practical actuator package
|
||||
# grows to about 59 mm OD so through-bolt bosses have enough wall around the head
|
||||
# counterbores instead of being cosmetic pin holes.
|
||||
HOUSING_BODY_OUTER_RADIUS = 24.2
|
||||
HOUSING_INNER_RADIUS = 21.70
|
||||
HOUSING_DATUM_INNER_RADIUS = 20.95
|
||||
HOUSING_DATUM_OUTER_RADIUS = 21.85
|
||||
HOUSING_BOTTOM_Z = -15.0
|
||||
HOUSING_HEIGHT = 30.0
|
||||
|
||||
# Output/input end-cap details seen on real joint actuators. The case fasteners
|
||||
# are modeled as real through holes through the housing stack, not as cosmetic
|
||||
# front-face pockets; the larger envelope is the cost of giving M3-class screws
|
||||
# enough boss diameter outside the planetary ring gear.
|
||||
HOUSING_FRONT_FLANGE_THICKNESS = 3.0
|
||||
HOUSING_REAR_FLANGE_THICKNESS = 3.0
|
||||
HOUSING_END_FLANGE_OUTER_RADIUS = 24.2
|
||||
|
||||
# The outer mounting structure is four continuous sector pads, each carrying
|
||||
# three full-length screws. This is closer to the reference actuator than twelve
|
||||
# isolated round ears: it preserves a scalloped circular silhouette, gives each
|
||||
# fastener real surrounding material, and leaves service gaps between quarters.
|
||||
HOUSING_MOUNT_SECTOR_COUNT = 4
|
||||
HOUSING_MOUNT_HOLES_PER_SECTOR = 3
|
||||
HOUSING_MOUNT_HOLE_COUNT = HOUSING_MOUNT_SECTOR_COUNT * HOUSING_MOUNT_HOLES_PER_SECTOR
|
||||
HOUSING_MOUNT_PAD_INNER_RADIUS = 23.6
|
||||
HOUSING_MOUNT_PAD_OUTER_RADIUS = HOUSING_OUTER_RADIUS
|
||||
HOUSING_MOUNT_SECTOR_GAP_WIDTH = 5.0
|
||||
HOUSING_MOUNT_SECTOR_CENTER_OFFSET_DEGREES = 45.0
|
||||
HOUSING_MOUNT_HOLE_OFFSET_DEGREES = 18.0
|
||||
HOUSING_MOUNT_HOLE_CIRCLE_RADIUS = 26.4
|
||||
HOUSING_MOUNT_HOLE_DIAMETER = 3.0
|
||||
HOUSING_MOUNT_COUNTERBORE_DIAMETER = 5.6
|
||||
HOUSING_MOUNT_COUNTERBORE_DEPTH = 1.2
|
||||
|
||||
# A sealed output needs a small controlled radial gap instead of the previous
|
||||
# large empty annulus between the rotating flange and fixed housing. The front
|
||||
# flange bore is the fixed labyrinth lip; it is not a separate part because that
|
||||
# would create a fully contained boolean feature with no extra assembly value.
|
||||
OUTPUT_SEAL_BORE_RADIUS = 20.65
|
||||
OUTPUT_SEAL_RUNNING_CLEARANCE = 0.30
|
||||
INPUT_SEAL_RUNNING_CLEARANCE = 0.30
|
||||
|
||||
INPUT_FLANGE_BOTTOM_Z = -15.0
|
||||
INPUT_FLANGE_THICKNESS = 2.0
|
||||
INPUT_FLANGE_BOSS_HEIGHT = 1.0
|
||||
INPUT_FLANGE_TOP_Z = INPUT_FLANGE_BOTTOM_Z + INPUT_FLANGE_THICKNESS + INPUT_FLANGE_BOSS_HEIGHT
|
||||
INPUT_FLANGE_OUTER_DIAMETER = 23.0
|
||||
INPUT_FLANGE_INNER_DIAMETER = 3.0
|
||||
INPUT_FLANGE_BOSS_OUTER_DIAMETER = 8.0
|
||||
INPUT_FLANGE_HOLE_DIAMETER = 1.6
|
||||
INPUT_FLANGE_HOLE_CIRCLE_DIAMETER = 17.0
|
||||
INPUT_FLANGE_HOLE_COUNT = 6
|
||||
INPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER = 3.0
|
||||
INPUT_FLANGE_HOLE_COUNTERBORE_DEPTH = 0.45
|
||||
INPUT_SEAL_BORE_RADIUS = INPUT_FLANGE_OUTER_DIAMETER / 2.0 + INPUT_SEAL_RUNNING_CLEARANCE
|
||||
|
||||
OUTPUT_FLANGE_BOTTOM_Z = 12.0
|
||||
OUTPUT_FLANGE_THICKNESS = 2.4
|
||||
OUTPUT_FLANGE_BOSS_HEIGHT = 0.6
|
||||
OUTPUT_FLANGE_TOP_Z = OUTPUT_FLANGE_BOTTOM_Z + OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_BOSS_HEIGHT
|
||||
OUTPUT_FLANGE_OUTER_DIAMETER = 40.7
|
||||
OUTPUT_FLANGE_INNER_DIAMETER = 3.4
|
||||
OUTPUT_FLANGE_BOSS_OUTER_DIAMETER = 12.8
|
||||
OUTPUT_FLANGE_HOLE_DIAMETER = 2.0
|
||||
OUTPUT_FLANGE_HOLE_CIRCLE_DIAMETER = 30.0
|
||||
OUTPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER = 3.2
|
||||
OUTPUT_FLANGE_HOLE_COUNTERBORE_DEPTH = 0.45
|
||||
|
||||
# The raised segmented register is not decorative. Real actuator output flanges
|
||||
# often use raised islands so the mating link can have matching recesses for fast
|
||||
# angular indexing and shear load location before the screws are tightened.
|
||||
OUTPUT_FLANGE_REGISTER_INNER_DIAMETER = 22.0
|
||||
OUTPUT_FLANGE_REGISTER_OUTER_DIAMETER = 35.0
|
||||
OUTPUT_FLANGE_REGISTER_HEIGHT = 0.45
|
||||
OUTPUT_FLANGE_REGISTER_PAD_COUNT = 3
|
||||
OUTPUT_FLANGE_REGISTER_GAP_WIDTH = 3.1
|
||||
OUTPUT_FLANGE_HOLES_PER_PAD = 2
|
||||
OUTPUT_FLANGE_HOLE_OFFSET_DEGREES = 18.0
|
||||
OUTPUT_FLANGE_HOLE_COUNT = OUTPUT_FLANGE_REGISTER_PAD_COUNT * OUTPUT_FLANGE_HOLES_PER_PAD
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_COUNT = 3
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_CIRCLE_DIAMETER = 10.5
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_DIAMETER = 1.2
|
||||
OUTPUT_FLANGE_CENTER_COUNTERBORE_DIAMETER = 2.1
|
||||
OUTPUT_FLANGE_CENTER_COUNTERBORE_DEPTH = 0.35
|
||||
|
||||
INPUT_SHAFT_RADIUS = 1.45
|
||||
STAGE1_CARRIER_SHAFT_RADIUS = 1.35
|
||||
OUTPUT_SHAFT_RADIUS = 1.50
|
||||
|
||||
STAGE1_CARRIER_PLATE_BOTTOM_Z = -3.25
|
||||
STAGE1_CARRIER_PLATE_THICKNESS = 1.65
|
||||
STAGE1_PIN_BOTTOM_Z = -8.15
|
||||
STAGE1_PIN_RADIUS = 1.10
|
||||
STAGE1_PIN_LAND_RADIUS = 1.18
|
||||
STAGE1_HUB_RADIUS = 3.40
|
||||
STAGE1_ARM_WIDTH = 2.50
|
||||
STAGE1_PAD_RADIUS = 4.10
|
||||
|
||||
STAGE2_CARRIER_PLATE_BOTTOM_Z = 6.45
|
||||
STAGE2_CARRIER_PLATE_THICKNESS = 1.65
|
||||
STAGE2_PIN_BOTTOM_Z = 1.45
|
||||
STAGE2_PIN_RADIUS = 0.82
|
||||
STAGE2_PIN_LAND_RADIUS = 0.93
|
||||
STAGE2_HUB_RADIUS = 3.35
|
||||
STAGE2_ARM_WIDTH = 2.35
|
||||
STAGE2_PAD_RADIUS = 3.20
|
||||
|
||||
INPUT_BEARING_Z = -11.0
|
||||
INTERMEDIATE_BEARING_Z = 0.0
|
||||
OUTPUT_BEARING_Z = 10.8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StageSpec:
|
||||
"""A tooth-count and axial-location spec for one planetary stage."""
|
||||
|
||||
stage_id: str
|
||||
label: str
|
||||
sun_teeth: int
|
||||
planet_teeth: int
|
||||
bottom_z: float
|
||||
sun_helix_angle: float
|
||||
|
||||
@property
|
||||
def ring_teeth(self) -> int:
|
||||
return self.sun_teeth + 2 * self.planet_teeth
|
||||
|
||||
@property
|
||||
def planet_helix_angle(self) -> float:
|
||||
return -self.sun_helix_angle
|
||||
|
||||
@property
|
||||
def ring_helix_angle(self) -> float:
|
||||
return self.planet_helix_angle
|
||||
|
||||
@property
|
||||
def gear_height(self) -> float:
|
||||
return GEAR_HEIGHT
|
||||
|
||||
@property
|
||||
def top_z(self) -> float:
|
||||
return self.bottom_z + self.gear_height
|
||||
|
||||
@property
|
||||
def mid_z(self) -> float:
|
||||
return self.bottom_z + self.gear_height / 2.0
|
||||
|
||||
@property
|
||||
def sun_pitch_radius(self) -> float:
|
||||
return MODULE * self.sun_teeth / 2.0
|
||||
|
||||
@property
|
||||
def planet_pitch_radius(self) -> float:
|
||||
return MODULE * self.planet_teeth / 2.0
|
||||
|
||||
@property
|
||||
def ring_pitch_radius(self) -> float:
|
||||
return MODULE * self.ring_teeth / 2.0
|
||||
|
||||
@property
|
||||
def planet_center_radius(self) -> float:
|
||||
return MODULE * (self.sun_teeth + self.planet_teeth) / 2.0
|
||||
|
||||
@property
|
||||
def fixed_ring_ratio(self) -> float:
|
||||
return 1.0 + self.ring_teeth / self.sun_teeth
|
||||
|
||||
@property
|
||||
def ring_outer_radius(self) -> float:
|
||||
tooth_root_allowance = MODULE * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
|
||||
return self.ring_pitch_radius + tooth_root_allowance + RING_RIM_THICKNESS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BearingSpec:
|
||||
"""A small radial ball bearing package."""
|
||||
|
||||
bore_diameter: float
|
||||
outer_diameter: float
|
||||
width: float
|
||||
ball_diameter: float
|
||||
ball_count: int
|
||||
raceway_clearance: float = 0.03
|
||||
edge_chamfer: float = 0.0
|
||||
|
||||
|
||||
STAGE_1 = StageSpec(
|
||||
stage_id="stage1",
|
||||
label="Stage 1",
|
||||
sun_teeth=12,
|
||||
planet_teeth=18,
|
||||
bottom_z=-8.40,
|
||||
sun_helix_angle=HELIX_ANGLE,
|
||||
)
|
||||
STAGE_2 = StageSpec(
|
||||
stage_id="stage2",
|
||||
label="Stage 2",
|
||||
sun_teeth=12,
|
||||
planet_teeth=12,
|
||||
bottom_z=1.20,
|
||||
sun_helix_angle=HELIX_ANGLE,
|
||||
)
|
||||
|
||||
UNIVERSAL_RADIAL_BEARING = BearingSpec(
|
||||
bore_diameter=3.2,
|
||||
outer_diameter=6.6,
|
||||
width=2.0,
|
||||
ball_diameter=0.55,
|
||||
ball_count=8,
|
||||
)
|
||||
INPUT_SHAFT_BEARING = UNIVERSAL_RADIAL_BEARING
|
||||
INTERMEDIATE_SHAFT_BEARING = UNIVERSAL_RADIAL_BEARING
|
||||
OUTPUT_SHAFT_BEARING = UNIVERSAL_RADIAL_BEARING
|
||||
STAGE1_PLANET_BEARING = UNIVERSAL_RADIAL_BEARING
|
||||
STAGE2_PLANET_BEARING = UNIVERSAL_RADIAL_BEARING
|
||||
|
||||
TOTAL_REDUCTION = STAGE_1.fixed_ring_ratio * STAGE_2.fixed_ring_ratio
|
||||
@@ -0,0 +1,365 @@
|
||||
"""Input and output flange parts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import _apply_tags, make_axis_part_rpart
|
||||
from dimensions import (
|
||||
INPUT_FLANGE_BOSS_HEIGHT,
|
||||
INPUT_FLANGE_BOSS_OUTER_DIAMETER,
|
||||
INPUT_FLANGE_BOTTOM_Z,
|
||||
INPUT_FLANGE_HOLE_CIRCLE_DIAMETER,
|
||||
INPUT_FLANGE_HOLE_COUNT,
|
||||
INPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
INPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER,
|
||||
INPUT_FLANGE_HOLE_DIAMETER,
|
||||
INPUT_FLANGE_INNER_DIAMETER,
|
||||
INPUT_FLANGE_OUTER_DIAMETER,
|
||||
INPUT_FLANGE_THICKNESS,
|
||||
INPUT_FLANGE_TOP_Z,
|
||||
OUTPUT_FLANGE_BOSS_HEIGHT,
|
||||
OUTPUT_FLANGE_BOSS_OUTER_DIAMETER,
|
||||
OUTPUT_FLANGE_BOTTOM_Z,
|
||||
OUTPUT_FLANGE_CENTER_COUNTERBORE_DEPTH,
|
||||
OUTPUT_FLANGE_CENTER_COUNTERBORE_DIAMETER,
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_CIRCLE_DIAMETER,
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_COUNT,
|
||||
OUTPUT_FLANGE_CENTER_FASTENER_DIAMETER,
|
||||
OUTPUT_FLANGE_HOLE_CIRCLE_DIAMETER,
|
||||
OUTPUT_FLANGE_HOLE_COUNT,
|
||||
OUTPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
OUTPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER,
|
||||
OUTPUT_FLANGE_HOLE_DIAMETER,
|
||||
OUTPUT_FLANGE_HOLE_OFFSET_DEGREES,
|
||||
OUTPUT_FLANGE_HOLES_PER_PAD,
|
||||
OUTPUT_FLANGE_INNER_DIAMETER,
|
||||
OUTPUT_FLANGE_OUTER_DIAMETER,
|
||||
OUTPUT_FLANGE_REGISTER_GAP_WIDTH,
|
||||
OUTPUT_FLANGE_REGISTER_HEIGHT,
|
||||
OUTPUT_FLANGE_REGISTER_INNER_DIAMETER,
|
||||
OUTPUT_FLANGE_REGISTER_OUTER_DIAMETER,
|
||||
OUTPUT_FLANGE_REGISTER_PAD_COUNT,
|
||||
OUTPUT_FLANGE_THICKNESS,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
)
|
||||
|
||||
|
||||
def make_input_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the reducer input flange part with six bolt holes."""
|
||||
|
||||
flange = _make_n_hole_flange_solid_rsolid(
|
||||
flange_outer_diameter=INPUT_FLANGE_OUTER_DIAMETER,
|
||||
flange_inner_diameter=INPUT_FLANGE_INNER_DIAMETER,
|
||||
flange_thickness=INPUT_FLANGE_THICKNESS,
|
||||
boss_outer_diameter=INPUT_FLANGE_BOSS_OUTER_DIAMETER,
|
||||
boss_height=INPUT_FLANGE_BOSS_HEIGHT,
|
||||
hole_diameter=INPUT_FLANGE_HOLE_DIAMETER,
|
||||
hole_circle_diameter=INPUT_FLANGE_HOLE_CIRCLE_DIAMETER,
|
||||
hole_count=INPUT_FLANGE_HOLE_COUNT,
|
||||
counterbore_diameter=INPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER,
|
||||
counterbore_depth=INPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
)
|
||||
flange = scad.translate_shape(
|
||||
shape=flange,
|
||||
vector=(0.0, 0.0, INPUT_FLANGE_BOTTOM_Z),
|
||||
)
|
||||
flange = _apply_tags(
|
||||
flange,
|
||||
tags=("role.input_flange", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"input_flange: outer_diameter={INPUT_FLANGE_OUTER_DIAMETER:.1f} "
|
||||
f"top_z={INPUT_FLANGE_TOP_Z:.3f} faces={len(flange.get_faces())}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="input_flange",
|
||||
solid=flange,
|
||||
name="Six-hole input flange",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": INPUT_FLANGE_TOP_Z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_output_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the reducer output flange part with realistic mounting detail."""
|
||||
|
||||
flange = _make_output_flange_solid_rsolid()
|
||||
flange = scad.translate_shape(
|
||||
shape=flange,
|
||||
vector=(0.0, 0.0, OUTPUT_FLANGE_BOTTOM_Z),
|
||||
)
|
||||
flange = _apply_tags(
|
||||
flange,
|
||||
tags=("role.output_flange", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"output_flange: outer_diameter={OUTPUT_FLANGE_OUTER_DIAMETER:.1f} "
|
||||
f"holes={OUTPUT_FLANGE_HOLE_COUNT} top_z={OUTPUT_FLANGE_TOP_Z:.3f} "
|
||||
f"faces={len(flange.get_faces())}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="output_flange",
|
||||
solid=flange,
|
||||
name="Six-hole output flange",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": OUTPUT_FLANGE_TOP_Z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
"""Build the sealed actuator-style output flange.
|
||||
|
||||
The earlier example used a small six-hole disk. That was enough to prove
|
||||
the gear train, but it did not describe how a robot link would actually find
|
||||
and fasten to the actuator. This output part keeps the simple reducer core
|
||||
while adding three production-oriented details: a broad rotating face close
|
||||
to the housing bore, segmented raised register pads for quick angular
|
||||
location, and separate center fasteners for retaining the output cap.
|
||||
"""
|
||||
|
||||
base = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_OUTER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
boss = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_BOSS_OUTER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_BOSS_HEIGHT + 0.05,
|
||||
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_THICKNESS - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
register_outer = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_REGISTER_OUTER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_REGISTER_HEIGHT + 0.05,
|
||||
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_THICKNESS - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
register_inner = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_REGISTER_INNER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_REGISTER_HEIGHT + 0.55,
|
||||
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_THICKNESS - 0.30),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
register = scad.cut_rsolid(register_outer, register_inner, skip_non_intersecting=False)
|
||||
|
||||
# The register ring is intentionally shallow and segmented. In real joint
|
||||
# modules this gives the mating link a positive anti-slip locating feature:
|
||||
# the link can have matching recesses, so torque is not carried only by screw
|
||||
# friction while the assembler is trying to align the output face.
|
||||
flange = scad.union_rsolid([base, boss, register], glue=False)
|
||||
|
||||
cutters = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_INNER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_BOSS_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
]
|
||||
|
||||
register_mid_radius = (
|
||||
OUTPUT_FLANGE_REGISTER_INNER_DIAMETER + OUTPUT_FLANGE_REGISTER_OUTER_DIAMETER
|
||||
) / 4.0
|
||||
register_radial_width = (
|
||||
OUTPUT_FLANGE_REGISTER_OUTER_DIAMETER - OUTPUT_FLANGE_REGISTER_INNER_DIAMETER
|
||||
) / 2.0
|
||||
for index in range(OUTPUT_FLANGE_REGISTER_PAD_COUNT):
|
||||
gap_angle = 60.0 + 360.0 * index / OUTPUT_FLANGE_REGISTER_PAD_COUNT
|
||||
gap = scad.make_box_rsolid(
|
||||
width=register_radial_width + 2.2,
|
||||
height=OUTPUT_FLANGE_REGISTER_GAP_WIDTH,
|
||||
depth=OUTPUT_FLANGE_REGISTER_HEIGHT + 0.6,
|
||||
bottom_face_center=(
|
||||
register_mid_radius,
|
||||
0.0,
|
||||
OUTPUT_FLANGE_THICKNESS - 0.25,
|
||||
),
|
||||
)
|
||||
cutters.append(
|
||||
scad.rotate_shape(
|
||||
shape=gap,
|
||||
angle=gap_angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
|
||||
output_bolt_radius = OUTPUT_FLANGE_HOLE_CIRCLE_DIAMETER / 2.0
|
||||
output_hole_angles = []
|
||||
for pad_index in range(OUTPUT_FLANGE_REGISTER_PAD_COUNT):
|
||||
pad_center_angle = 360.0 * pad_index / OUTPUT_FLANGE_REGISTER_PAD_COUNT
|
||||
for hole_index in range(OUTPUT_FLANGE_HOLES_PER_PAD):
|
||||
side = -1.0 if hole_index == 0 else 1.0
|
||||
output_hole_angles.append(pad_center_angle + side * OUTPUT_FLANGE_HOLE_OFFSET_DEGREES)
|
||||
|
||||
for angle_degrees in output_hole_angles:
|
||||
angle = math.radians(angle_degrees)
|
||||
x = output_bolt_radius * math.cos(angle)
|
||||
y = output_bolt_radius * math.sin(angle)
|
||||
|
||||
# These holes sit on the raised pads rather than on a flat disk. That is
|
||||
# the visible design cue from the reference actuator: the pad geometry is
|
||||
# a locating interface, and the screws clamp through that known land.
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_HOLE_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_REGISTER_HEIGHT + 1.0,
|
||||
bottom_face_center=(x, y, -0.5),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_HOLE_COUNTERBORE_DEPTH + 0.3,
|
||||
bottom_face_center=(
|
||||
x,
|
||||
y,
|
||||
OUTPUT_FLANGE_THICKNESS
|
||||
+ OUTPUT_FLANGE_REGISTER_HEIGHT
|
||||
- OUTPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
cap_bolt_radius = OUTPUT_FLANGE_CENTER_FASTENER_CIRCLE_DIAMETER / 2.0
|
||||
for index in range(OUTPUT_FLANGE_CENTER_FASTENER_COUNT):
|
||||
angle = 2.0 * math.pi * index / OUTPUT_FLANGE_CENTER_FASTENER_COUNT + math.radians(30.0)
|
||||
x = cap_bolt_radius * math.cos(angle)
|
||||
y = cap_bolt_radius * math.sin(angle)
|
||||
|
||||
# The center screws read as output-cap retention hardware. Keeping them
|
||||
# separate from the larger link-mount holes mirrors real actuator stackups:
|
||||
# service screws retain the internal cap; larger screws attach the robot.
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_CENTER_FASTENER_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_BOSS_HEIGHT + 1.0,
|
||||
bottom_face_center=(x, y, -0.5),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_CENTER_COUNTERBORE_DIAMETER / 2.0,
|
||||
height=OUTPUT_FLANGE_CENTER_COUNTERBORE_DEPTH + 0.3,
|
||||
bottom_face_center=(
|
||||
x,
|
||||
y,
|
||||
OUTPUT_FLANGE_THICKNESS
|
||||
+ OUTPUT_FLANGE_BOSS_HEIGHT
|
||||
- OUTPUT_FLANGE_CENTER_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
flange = scad.cut_rsolid(flange, cutters, skip_non_intersecting=False)
|
||||
flange = _apply_tags(
|
||||
flange,
|
||||
tags=("role.output_register_pads", "role.link_mount_interface"),
|
||||
)
|
||||
print(
|
||||
f"output_flange_core: od={OUTPUT_FLANGE_OUTER_DIAMETER:.1f} "
|
||||
f"register_pads={OUTPUT_FLANGE_REGISTER_PAD_COUNT} link_holes={len(output_hole_angles)} "
|
||||
f"cap_holes={OUTPUT_FLANGE_CENTER_FASTENER_COUNT} faces={len(flange.get_faces())} "
|
||||
f"volume={flange.get_volume():.3f}"
|
||||
)
|
||||
return flange
|
||||
|
||||
|
||||
def _make_n_hole_flange_solid_rsolid(
|
||||
*,
|
||||
flange_outer_diameter: float,
|
||||
flange_inner_diameter: float,
|
||||
flange_thickness: float,
|
||||
boss_outer_diameter: float,
|
||||
boss_height: float,
|
||||
hole_diameter: float,
|
||||
hole_circle_diameter: float,
|
||||
hole_count: int,
|
||||
counterbore_diameter: float | None = None,
|
||||
counterbore_depth: float = 0.0,
|
||||
) -> scad.Solid:
|
||||
"""Build a flange without edge-pick features so FreeCAD export is stable."""
|
||||
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=flange_outer_diameter / 2.0,
|
||||
height=flange_thickness,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
boss = scad.make_cylinder_rsolid(
|
||||
radius=boss_outer_diameter / 2.0,
|
||||
height=boss_height + 0.05,
|
||||
bottom_face_center=(0.0, 0.0, flange_thickness - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
flange = scad.union_rsolid([outer, boss], glue=False)
|
||||
|
||||
cutters = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=flange_inner_diameter / 2.0,
|
||||
height=flange_thickness + boss_height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
]
|
||||
bolt_circle_radius = hole_circle_diameter / 2.0
|
||||
for index in range(hole_count):
|
||||
angle = 2.0 * math.pi * index / hole_count
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=hole_diameter / 2.0,
|
||||
height=flange_thickness + boss_height + 2.0,
|
||||
bottom_face_center=(
|
||||
bolt_circle_radius * math.cos(angle),
|
||||
bolt_circle_radius * math.sin(angle),
|
||||
-1.0,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
if counterbore_diameter is not None and counterbore_depth > 0.0:
|
||||
# Even the input-side service flange gets proper screw head relief.
|
||||
# Otherwise the front end looks realistic while the motor/input side
|
||||
# remains a bare demo disk with no way to sit flush against a cover.
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=counterbore_diameter / 2.0,
|
||||
height=counterbore_depth + 0.3,
|
||||
bottom_face_center=(
|
||||
bolt_circle_radius * math.cos(angle),
|
||||
bolt_circle_radius * math.sin(angle),
|
||||
flange_thickness - counterbore_depth,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
flange = scad.cut_rsolid(flange, cutters, skip_non_intersecting=False)
|
||||
print(
|
||||
f"flange_core: od={flange_outer_diameter:.1f} id={flange_inner_diameter:.1f} "
|
||||
f"holes={hole_count} hole_d={hole_diameter:.1f} faces={len(flange.get_faces())} "
|
||||
f"volume={flange.get_volume():.3f}"
|
||||
)
|
||||
return flange
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Reusable herringbone gear parts for the two-stage reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
from common import (
|
||||
_apply_tags,
|
||||
add_placement_axis_connector_rpart,
|
||||
make_axis_part_rpart,
|
||||
make_z_rotation_rplacement,
|
||||
)
|
||||
from dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CLEARANCE_FACTOR,
|
||||
FIXED_RING_HOUSING_SUPPORT_OVERLAP,
|
||||
GEAR_HEIGHT,
|
||||
HOUSING_INNER_RADIUS,
|
||||
MODULE,
|
||||
PRESSURE_ANGLE,
|
||||
RING_RIM_THICKNESS,
|
||||
BearingSpec,
|
||||
StageSpec,
|
||||
)
|
||||
|
||||
|
||||
def make_stage_ring_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create one fixed internal herringbone ring gear part for a stage."""
|
||||
|
||||
ring = scad.std.gear.make_herringbone_ring_gear_rsolid(
|
||||
n_teeth=stage.ring_teeth,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=stage.ring_helix_angle,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RING_RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
)
|
||||
support = scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_INNER_RADIUS,
|
||||
height=stage.gear_height,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
support_bore = scad.make_cylinder_rsolid(
|
||||
radius=stage.ring_outer_radius - FIXED_RING_HOUSING_SUPPORT_OVERLAP,
|
||||
height=stage.gear_height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
support = scad.cut_rsolid(
|
||||
support,
|
||||
support_bore,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
support = scad.apply_tag(shape=support, tag=f"role.{stage.stage_id}.fixed_ring_housing_support")
|
||||
ring = scad.union_rsolid([ring, support], glue=False)
|
||||
ring = _apply_tags(
|
||||
ring,
|
||||
tags=(f"role.{stage.stage_id}.fixed_ring_gear", "group.two_stage_reducer"),
|
||||
)
|
||||
_ground_gear(label=f"{stage.stage_id}_ring", solid=ring)
|
||||
print(
|
||||
f"{stage.stage_id}_ring_pitch: teeth={stage.ring_teeth} "
|
||||
f"pitch_radius={stage.ring_pitch_radius:.3f} outer_radius={stage.ring_outer_radius:.3f} "
|
||||
f"support_outer_radius={HOUSING_INNER_RADIUS:.3f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_ring_gear",
|
||||
solid=ring,
|
||||
name=f"{stage.label} fixed herringbone ring gear",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": GEAR_HEIGHT,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_stage_sun_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
bore_radius: float,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create one bored external herringbone sun gear part for a stage."""
|
||||
|
||||
sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=stage.sun_teeth,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=stage.sun_helix_angle,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
sun = _cut_bore_rsolid(
|
||||
label=f"{stage.stage_id}_sun_bore",
|
||||
solid=sun,
|
||||
bore_radius=bore_radius,
|
||||
)
|
||||
sun = _apply_tags(
|
||||
sun,
|
||||
tags=(f"role.{stage.stage_id}.sun_gear", "group.two_stage_reducer"),
|
||||
)
|
||||
_ground_gear(label=f"{stage.stage_id}_sun", solid=sun)
|
||||
print(
|
||||
f"{stage.stage_id}_sun_pitch: teeth={stage.sun_teeth} "
|
||||
f"pitch_radius={stage.sun_pitch_radius:.3f} bore_radius={bore_radius:.3f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_sun_gear",
|
||||
solid=sun,
|
||||
name=f"{stage.label} herringbone sun gear",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": GEAR_HEIGHT,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_stage_planet_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
bearing: BearingSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create a reusable bored herringbone planet gear part for a stage."""
|
||||
|
||||
planet = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=stage.planet_teeth,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=stage.planet_helix_angle,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
bore_radius = bearing.outer_diameter / 2.0 + 0.06
|
||||
planet = _cut_bore_rsolid(
|
||||
label=f"{stage.stage_id}_planet_bearing_seat",
|
||||
solid=planet,
|
||||
bore_radius=bore_radius,
|
||||
)
|
||||
planet = _apply_tags(
|
||||
planet,
|
||||
tags=(f"role.{stage.stage_id}.planet_gear", "group.two_stage_reducer"),
|
||||
)
|
||||
_ground_gear(label=f"{stage.stage_id}_planet", solid=planet)
|
||||
print(
|
||||
f"{stage.stage_id}_planet_pitch: teeth={stage.planet_teeth} "
|
||||
f"pitch_radius={stage.planet_pitch_radius:.3f} bearing_seat_radius={bore_radius:.3f}"
|
||||
)
|
||||
part = make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_planet_gear",
|
||||
solid=planet,
|
||||
name=f"{stage.label} reusable herringbone planet gear",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": GEAR_HEIGHT,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
return add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id="bearing_axis",
|
||||
origin=(0.0, 0.0, stage.gear_height / 2.0),
|
||||
name=f"{stage.label} planet bearing bore axis",
|
||||
)
|
||||
|
||||
|
||||
def make_planet_component_rplacement(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
planet_index: int,
|
||||
) -> scad.Placement:
|
||||
"""Return the placed and phased component placement for one planet gear."""
|
||||
|
||||
carrier_angle = 360.0 * planet_index / 3.0
|
||||
angle_radians = math.radians(carrier_angle)
|
||||
center = (
|
||||
stage.planet_center_radius * math.cos(angle_radians),
|
||||
stage.planet_center_radius * math.sin(angle_radians),
|
||||
stage.bottom_z,
|
||||
)
|
||||
planet_spin = carrier_angle + 180.0 - (180.0 / stage.planet_teeth)
|
||||
print(
|
||||
f"{stage.stage_id}_planet_{planet_index + 1}: center=({center[0]:.3f},{center[1]:.3f},{center[2]:.3f}) "
|
||||
f"carrier_angle={carrier_angle:.1f} spin={planet_spin:.1f}"
|
||||
)
|
||||
return make_z_rotation_rplacement(origin=center, angle_degrees=planet_spin)
|
||||
|
||||
|
||||
def _cut_bore_rsolid(
|
||||
*,
|
||||
label: str,
|
||||
solid: scad.Solid,
|
||||
bore_radius: float,
|
||||
) -> scad.Solid:
|
||||
cutter = scad.make_cylinder_rsolid(
|
||||
radius=bore_radius,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bored = scad.cut_rsolid(solid, cutter, skip_non_intersecting=False)
|
||||
bored = scad.apply_tag(shape=bored, tag=f"solid.cut.{label}")
|
||||
print(f"{label}: bore_radius={bore_radius:.3f} volume={bored.get_volume():.3f}")
|
||||
return bored
|
||||
|
||||
|
||||
def _ground_gear(*, label: str, solid: scad.Solid) -> None:
|
||||
faces = ql.select(items=solid.get_faces()).all()
|
||||
edges = ql.select(items=solid.get_edges()).all()
|
||||
print(
|
||||
f"gear_{label}: faces={len(faces)} edges={len(edges)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
@@ -0,0 +1,310 @@
|
||||
"""Reducer housing sleeve and fixed-axis connector datums."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import (
|
||||
_apply_tags,
|
||||
add_placement_axis_connector_rpart,
|
||||
make_annular_cylinder_rsolid,
|
||||
)
|
||||
from dimensions import (
|
||||
HOUSING_BODY_OUTER_RADIUS,
|
||||
HOUSING_DATUM_INNER_RADIUS,
|
||||
HOUSING_DATUM_OUTER_RADIUS,
|
||||
HOUSING_END_FLANGE_OUTER_RADIUS,
|
||||
HOUSING_FRONT_FLANGE_THICKNESS,
|
||||
HOUSING_HEIGHT,
|
||||
HOUSING_INNER_RADIUS,
|
||||
HOUSING_MOUNT_COUNTERBORE_DEPTH,
|
||||
HOUSING_MOUNT_COUNTERBORE_DIAMETER,
|
||||
HOUSING_MOUNT_HOLE_CIRCLE_RADIUS,
|
||||
HOUSING_MOUNT_HOLE_COUNT,
|
||||
HOUSING_MOUNT_HOLE_DIAMETER,
|
||||
HOUSING_MOUNT_HOLE_OFFSET_DEGREES,
|
||||
HOUSING_MOUNT_HOLES_PER_SECTOR,
|
||||
HOUSING_MOUNT_PAD_INNER_RADIUS,
|
||||
HOUSING_MOUNT_PAD_OUTER_RADIUS,
|
||||
HOUSING_MOUNT_SECTOR_CENTER_OFFSET_DEGREES,
|
||||
HOUSING_MOUNT_SECTOR_COUNT,
|
||||
HOUSING_MOUNT_SECTOR_GAP_WIDTH,
|
||||
HOUSING_OUTER_RADIUS,
|
||||
HOUSING_BOTTOM_Z,
|
||||
HOUSING_REAR_FLANGE_THICKNESS,
|
||||
INPUT_BEARING_Z,
|
||||
INPUT_FLANGE_TOP_Z,
|
||||
INPUT_SEAL_BORE_RADIUS,
|
||||
INPUT_SEAL_RUNNING_CLEARANCE,
|
||||
INTERMEDIATE_BEARING_Z,
|
||||
OUTPUT_BEARING_Z,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
OUTPUT_SEAL_BORE_RADIUS,
|
||||
OUTPUT_SEAL_RUNNING_CLEARANCE,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
)
|
||||
|
||||
|
||||
def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the through-bolted housing sleeve with internal datum collars."""
|
||||
|
||||
sleeve = make_annular_cylinder_rsolid(
|
||||
outer_radius=HOUSING_BODY_OUTER_RADIUS,
|
||||
inner_radius=HOUSING_INNER_RADIUS,
|
||||
height=HOUSING_HEIGHT,
|
||||
bottom_z=HOUSING_BOTTOM_Z,
|
||||
tag="role.housing_sleeve",
|
||||
)
|
||||
front_flange = _make_end_flange_rsolid(
|
||||
label="front",
|
||||
inner_radius=OUTPUT_SEAL_BORE_RADIUS,
|
||||
thickness=HOUSING_FRONT_FLANGE_THICKNESS,
|
||||
bottom_z=HOUSING_BOTTOM_Z + HOUSING_HEIGHT - HOUSING_FRONT_FLANGE_THICKNESS,
|
||||
)
|
||||
rear_flange = _make_end_flange_rsolid(
|
||||
label="rear",
|
||||
inner_radius=INPUT_SEAL_BORE_RADIUS,
|
||||
thickness=HOUSING_REAR_FLANGE_THICKNESS,
|
||||
bottom_z=HOUSING_BOTTOM_Z,
|
||||
)
|
||||
mount_pad = _make_mount_sector_pad_rsolid()
|
||||
collars = []
|
||||
datum_zs = (
|
||||
INPUT_FLANGE_TOP_Z,
|
||||
STAGE_1.top_z,
|
||||
STAGE_2.top_z,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
)
|
||||
for index, target_z in enumerate(datum_zs):
|
||||
# The end caps themselves provide the input/output seal lands. Avoid
|
||||
# adding fully contained datum collars at those end faces; they carry no
|
||||
# new mechanical information and make downstream translators less robust.
|
||||
if target_z <= HOUSING_BOTTOM_Z + HOUSING_REAR_FLANGE_THICKNESS:
|
||||
continue
|
||||
if target_z >= HOUSING_BOTTOM_Z + HOUSING_HEIGHT - HOUSING_FRONT_FLANGE_THICKNESS:
|
||||
continue
|
||||
collar = make_annular_cylinder_rsolid(
|
||||
outer_radius=HOUSING_DATUM_OUTER_RADIUS,
|
||||
inner_radius=HOUSING_DATUM_INNER_RADIUS,
|
||||
height=0.36,
|
||||
bottom_z=target_z - 0.36,
|
||||
tag=f"role.housing_axis_datum_{index + 1}",
|
||||
)
|
||||
collars.append(collar)
|
||||
|
||||
# The housing now has real through-bolt sector pads. The full-height pad is
|
||||
# cut after union, so each screw path clears both the visible pad and the
|
||||
# underlying housing body instead of stopping at a cosmetic front pocket.
|
||||
housing = scad.union_rsolid([sleeve, front_flange, rear_flange, mount_pad, collars], glue=False)
|
||||
housing = scad.cut_rsolid(
|
||||
housing,
|
||||
[
|
||||
_make_mount_gap_cutters_rsolids(),
|
||||
_make_mount_hole_cutters_rsolids(),
|
||||
],
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
housing = _apply_tags(
|
||||
housing,
|
||||
tags=("role.fixed_housing", "role.case_to_link_interface", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"housing_through_bolts: sectors={HOUSING_MOUNT_SECTOR_COUNT} holes={HOUSING_MOUNT_HOLE_COUNT} "
|
||||
f"hole_d={HOUSING_MOUNT_HOLE_DIAMETER:.1f} counterbore_d={HOUSING_MOUNT_COUNTERBORE_DIAMETER:.1f}"
|
||||
)
|
||||
print(
|
||||
f"housing_envelope: diameter={HOUSING_OUTER_RADIUS * 2.0:.1f} "
|
||||
f"height={HOUSING_HEIGHT:.1f} datum_count={len(datum_zs)} faces={len(housing.get_faces())}"
|
||||
)
|
||||
part = scad.make_part_rpart(
|
||||
part_id="reducer_housing",
|
||||
body=housing,
|
||||
name="Compact fixed reducer housing sleeve",
|
||||
)
|
||||
part = scad.assign_material_rpart(part=part, material=material)
|
||||
# The scalloped sector pads deliberately make the output face non-simple.
|
||||
# Housing axes are design datums, not manufactured face picks, so keep these
|
||||
# connectors topology-free for stable replay and FreeCAD translation.
|
||||
for connector_id, z in (
|
||||
("input_axis", STAGE_1.top_z),
|
||||
("stage1_axis", STAGE_1.top_z),
|
||||
("stage2_axis", STAGE_2.top_z),
|
||||
("output_axis", OUTPUT_FLANGE_TOP_Z),
|
||||
):
|
||||
part = add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id=connector_id,
|
||||
origin=(0.0, 0.0, z),
|
||||
name=connector_id.replace("_", " "),
|
||||
)
|
||||
for connector_id, z in (
|
||||
("input_bearing_axis", INPUT_BEARING_Z),
|
||||
("intermediate_bearing_axis", INTERMEDIATE_BEARING_Z),
|
||||
("output_bearing_axis", OUTPUT_BEARING_Z),
|
||||
):
|
||||
part = add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id=connector_id,
|
||||
origin=(0.0, 0.0, z),
|
||||
name=connector_id.replace("_", " "),
|
||||
)
|
||||
print(f"part_reducer_housing: connectors={len(part.connectors)} material=True")
|
||||
return part
|
||||
|
||||
|
||||
def _make_end_flange_rsolid(
|
||||
*,
|
||||
label: str,
|
||||
inner_radius: float,
|
||||
thickness: float,
|
||||
bottom_z: float,
|
||||
) -> scad.Solid:
|
||||
"""Build one sealed housing end cap.
|
||||
|
||||
The end cap is only the annular plate around the rotating input/output
|
||||
flange. Housing screws live in the through-bolt columns, not in small
|
||||
half-depth pockets on this cap.
|
||||
"""
|
||||
|
||||
flange = make_annular_cylinder_rsolid(
|
||||
outer_radius=HOUSING_END_FLANGE_OUTER_RADIUS,
|
||||
inner_radius=inner_radius,
|
||||
height=thickness,
|
||||
bottom_z=bottom_z,
|
||||
tag=f"role.housing_{label}_sealed_end_cap",
|
||||
)
|
||||
clearance = OUTPUT_SEAL_RUNNING_CLEARANCE if label == "front" else INPUT_SEAL_RUNNING_CLEARANCE
|
||||
print(
|
||||
f"{label}_seal_land: bore_radius={inner_radius:.2f} clearance={clearance:.2f}"
|
||||
)
|
||||
return flange
|
||||
|
||||
|
||||
def _make_mount_sector_pad_rsolid() -> scad.Solid:
|
||||
"""Build four graceful full-height sector pads before the global hole cut."""
|
||||
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_MOUNT_PAD_OUTER_RADIUS,
|
||||
height=HOUSING_HEIGHT,
|
||||
bottom_face_center=(0.0, 0.0, HOUSING_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
inner = scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_MOUNT_PAD_INNER_RADIUS,
|
||||
height=HOUSING_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, HOUSING_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
pad = scad.cut_rsolid(outer, inner, skip_non_intersecting=False)
|
||||
pad = _apply_tags(
|
||||
pad,
|
||||
tags=("role.housing_sector_mount_pads", "role.case_to_link_interface"),
|
||||
)
|
||||
print(
|
||||
f"housing_sector_pads: sectors={HOUSING_MOUNT_SECTOR_COUNT} "
|
||||
f"holes_per_sector={HOUSING_MOUNT_HOLES_PER_SECTOR} "
|
||||
f"outer_diameter={HOUSING_MOUNT_PAD_OUTER_RADIUS * 2.0:.1f}"
|
||||
)
|
||||
return pad
|
||||
|
||||
|
||||
def _make_mount_gap_cutters_rsolids() -> list[scad.Solid]:
|
||||
"""Build shallow radial gap cutters that divide the outer band into sectors."""
|
||||
|
||||
cutters = []
|
||||
gap_inner_radius = HOUSING_BODY_OUTER_RADIUS + 0.15
|
||||
gap_center_radius = (gap_inner_radius + HOUSING_MOUNT_PAD_OUTER_RADIUS + 0.8) / 2.0
|
||||
gap_radial_depth = HOUSING_MOUNT_PAD_OUTER_RADIUS - gap_inner_radius + 1.0
|
||||
for index in range(HOUSING_MOUNT_SECTOR_COUNT):
|
||||
gap_angle = (
|
||||
HOUSING_MOUNT_SECTOR_CENTER_OFFSET_DEGREES
|
||||
+ 45.0
|
||||
+ 360.0 * index / HOUSING_MOUNT_SECTOR_COUNT
|
||||
)
|
||||
gap = scad.make_box_rsolid(
|
||||
width=gap_radial_depth,
|
||||
height=HOUSING_MOUNT_SECTOR_GAP_WIDTH,
|
||||
depth=HOUSING_HEIGHT + 2.0,
|
||||
bottom_face_center=(gap_center_radius, 0.0, HOUSING_BOTTOM_Z - 1.0),
|
||||
)
|
||||
cutters.append(
|
||||
scad.rotate_shape(
|
||||
shape=gap,
|
||||
angle=gap_angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
|
||||
# These cuts shape only the outer mounting band. The inner housing shell is
|
||||
# intentionally left continuous, so the final part remains one case instead
|
||||
# of four separate ears connected only by fasteners.
|
||||
return cutters
|
||||
|
||||
|
||||
def _make_mount_hole_cutters_rsolids() -> list[scad.Solid]:
|
||||
"""Build one shared cutter set for the boss and housing body holes."""
|
||||
|
||||
cutters = []
|
||||
for sector_index in range(HOUSING_MOUNT_SECTOR_COUNT):
|
||||
sector_angle_degrees = (
|
||||
HOUSING_MOUNT_SECTOR_CENTER_OFFSET_DEGREES
|
||||
+ 360.0 * sector_index / HOUSING_MOUNT_SECTOR_COUNT
|
||||
)
|
||||
offsets = (
|
||||
-HOUSING_MOUNT_HOLE_OFFSET_DEGREES,
|
||||
0.0,
|
||||
HOUSING_MOUNT_HOLE_OFFSET_DEGREES,
|
||||
)
|
||||
for hole_index in range(HOUSING_MOUNT_HOLES_PER_SECTOR):
|
||||
angle_degrees = sector_angle_degrees + offsets[hole_index]
|
||||
angle = math.radians(angle_degrees)
|
||||
cutters.extend(_make_single_mount_hole_cutters_rsolids(angle=angle))
|
||||
return cutters
|
||||
|
||||
|
||||
def _make_single_mount_hole_cutters_rsolids(*, angle: float) -> list[scad.Solid]:
|
||||
"""Build through and counterbore cutters for one housing screw."""
|
||||
|
||||
x = HOUSING_MOUNT_HOLE_CIRCLE_RADIUS * math.cos(angle)
|
||||
y = HOUSING_MOUNT_HOLE_CIRCLE_RADIUS * math.sin(angle)
|
||||
cutters = []
|
||||
|
||||
# The through cutter spans the entire housing. If this stops short, the
|
||||
# front view still looks like a screw hole, but a real screw would hit the
|
||||
# rear half of the case exactly as the review screenshot showed.
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_MOUNT_HOLE_DIAMETER / 2.0,
|
||||
height=HOUSING_HEIGHT + 2.0,
|
||||
bottom_face_center=(x, y, HOUSING_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
# Counterbores are added on both ends so the actuator can be mounted from
|
||||
# either side during integration or service. This also gives enough head
|
||||
# diameter to visually read as an M3-class fastener interface.
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_MOUNT_COUNTERBORE_DIAMETER / 2.0,
|
||||
height=HOUSING_MOUNT_COUNTERBORE_DEPTH + 0.4,
|
||||
bottom_face_center=(
|
||||
x,
|
||||
y,
|
||||
HOUSING_BOTTOM_Z + HOUSING_HEIGHT - HOUSING_MOUNT_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=HOUSING_MOUNT_COUNTERBORE_DIAMETER / 2.0,
|
||||
height=HOUSING_MOUNT_COUNTERBORE_DEPTH + 0.4,
|
||||
bottom_face_center=(x, y, HOUSING_BOTTOM_Z - 0.2),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Build, validate, and export the compact two-stage planetary reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from assembly import make_two_stage_planetary_reducer_rassembly
|
||||
from common import _ground_compound
|
||||
from dimensions import HOUSING_HEIGHT, HOUSING_OUTER_RADIUS, TOTAL_REDUCTION
|
||||
|
||||
|
||||
# Herringbone gear profile graphs are intentionally deep.
|
||||
sys.setrecursionlimit(30000)
|
||||
|
||||
OUT_DIR = Path("examples/out/compact_two_stage_planetary_reducer")
|
||||
|
||||
|
||||
def _build_compact_two_stage_planetary_reducer():
|
||||
"""Build the reducer and return assembly, preview compound, and JSON exports."""
|
||||
|
||||
with scad.GraphSession(graph_id="compact_two_stage_planetary_reducer") as session:
|
||||
assembly = make_two_stage_planetary_reducer_rassembly()
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
_ground_compound(label="reducer_preview", compound=preview)
|
||||
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:
|
||||
"""Generate replayable JSON and STEP output for the reducer example."""
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
model_path = OUT_DIR / "compact_two_stage_planetary_reducer.model.json"
|
||||
session_path = OUT_DIR / "compact_two_stage_planetary_reducer.session.json"
|
||||
step_path = OUT_DIR / "compact_two_stage_planetary_reducer.step"
|
||||
fcstd_path = OUT_DIR / "compact_two_stage_planetary_reducer.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json, session_json = _build_compact_two_stage_planetary_reducer()
|
||||
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="CompactTwoStagePlanetaryReducer",
|
||||
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})"
|
||||
|
||||
solids = preview.get_solids()
|
||||
print(f"envelope_diameter={HOUSING_OUTER_RADIUS * 2.0:.1f}")
|
||||
print(f"envelope_height={HOUSING_HEIGHT:.1f}")
|
||||
print(f"total_reduction={TOTAL_REDUCTION:.1f}")
|
||||
print(f"assembly={assembly.assembly_id}")
|
||||
print("components=" + ",".join(assembly.component_ids()))
|
||||
print("constraints=" + ",".join(assembly.constraint_ids()))
|
||||
print(f"preview_solids={len(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()
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Material definitions for the compact reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def make_reducer_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create the small set of reusable material records for the assembly."""
|
||||
|
||||
materials = {
|
||||
"housing": scad.make_material_rmaterial(
|
||||
material_id="hard_anodized_aluminum",
|
||||
name="Hard anodized aluminum",
|
||||
density=2.70e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.26, 0.28, 0.30),
|
||||
),
|
||||
"carrier": scad.make_material_rmaterial(
|
||||
material_id="aluminum_7075",
|
||||
name="7075 aluminum carrier",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.48, 0.50, 0.52),
|
||||
),
|
||||
"gear": scad.make_material_rmaterial(
|
||||
material_id="case_hardened_steel",
|
||||
name="Case hardened gear steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.68, 0.70, 0.72),
|
||||
),
|
||||
"shaft": scad.make_material_rmaterial(
|
||||
material_id="tempered_shaft_steel",
|
||||
name="Tempered shaft steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.55, 0.57, 0.60),
|
||||
),
|
||||
}
|
||||
print("materials: " + ",".join(material.material_id for material in materials.values()))
|
||||
return materials
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Input shaft for the first planetary sun."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import _apply_tags, add_placement_axis_connector_rpart, make_axis_part_rpart
|
||||
from dimensions import INPUT_BEARING_Z, INPUT_FLANGE_TOP_Z, INPUT_SHAFT_RADIUS, STAGE_1
|
||||
|
||||
|
||||
def make_input_shaft_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the input shaft linking the input flange and stage 1 sun."""
|
||||
|
||||
height = STAGE_1.top_z - INPUT_FLANGE_TOP_Z
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=INPUT_SHAFT_RADIUS,
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, INPUT_FLANGE_TOP_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
shaft = _apply_tags(
|
||||
shaft,
|
||||
tags=("role.input_shaft", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"input_shaft: radius={INPUT_SHAFT_RADIUS:.3f} bottom_z={INPUT_FLANGE_TOP_Z:.3f} "
|
||||
f"top_z={STAGE_1.top_z:.3f} volume={shaft.get_volume():.3f}"
|
||||
)
|
||||
part = make_axis_part_rpart(
|
||||
part_id="input_shaft",
|
||||
solid=shaft,
|
||||
name="Input shaft to first-stage sun",
|
||||
material=material,
|
||||
connector_specs=(
|
||||
{
|
||||
"connector_id": "flange_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": INPUT_FLANGE_TOP_Z,
|
||||
"normal_z": -1.0,
|
||||
"flip": True,
|
||||
},
|
||||
{
|
||||
"connector_id": "sun_axis",
|
||||
"center_xy": (0.0, 0.0),
|
||||
"target_z": STAGE_1.top_z,
|
||||
"normal_z": 1.0,
|
||||
},
|
||||
),
|
||||
)
|
||||
return add_placement_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id="input_bearing_axis",
|
||||
origin=(0.0, 0.0, INPUT_BEARING_Z),
|
||||
name="Input bearing shaft seat axis",
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Example 17: static current-pose collision verification.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/17_static_collision_verifier.py
|
||||
|
||||
This example checks the current placements of two box components. The verifier
|
||||
uses internal cached meshes and python-fcl to report mesh contact penetration
|
||||
deeper than the configured tolerance. It does not solve constraints or detect
|
||||
complete containment cases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def _box_part() -> scad.Part:
|
||||
body = scad.make_box_rsolid(width=1.0, height=1.0, depth=1.0)
|
||||
return scad.make_part_rpart(part_id="unit_box", body=body)
|
||||
|
||||
|
||||
def _assembly_with_offset(offset: tuple[float, float, float]) -> scad.Assembly:
|
||||
part = _box_part()
|
||||
assembly = scad.make_assembly_rassembly(assembly_id="static_collision_demo")
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=part,
|
||||
component_id="box_a",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
assembly = scad.add_component_rassembly(
|
||||
assembly=assembly,
|
||||
item=part,
|
||||
component_id="box_b",
|
||||
placement=scad.make_placement_rplacement(origin=offset),
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _print_report(label: str, report: scad.verifier.CollisionReport) -> None:
|
||||
print(label, f"completed={report.completed}", f"passed={report.passed}")
|
||||
print(label, f"checked_pairs={report.checked_pair_count}", f"failures={report.failed_pair_count}")
|
||||
for failure in report.failures:
|
||||
print(
|
||||
label,
|
||||
"failure",
|
||||
"/".join(failure.component_a),
|
||||
"/".join(failure.component_b),
|
||||
f"depth={failure.penetration_depth:.4f}",
|
||||
f"allowed={failure.allowed_penetration:.4f}",
|
||||
)
|
||||
for warning in report.warnings:
|
||||
print(label, "warning", warning.code, warning.message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
config = scad.verifier.CollisionCheckConfig(max_allowed_penetration=0.01)
|
||||
|
||||
separated = _assembly_with_offset(offset=(2.0, 0.0, 0.0))
|
||||
separated_report = scad.verifier.check_collision_rcollisionreport(
|
||||
assembly=separated,
|
||||
config=config,
|
||||
)
|
||||
_print_report("separated", separated_report)
|
||||
|
||||
overlapping = _assembly_with_offset(offset=(0.5, 0.0, 0.0))
|
||||
overlapping_report = scad.verifier.check_collision_rcollisionreport(
|
||||
assembly=overlapping,
|
||||
config=config,
|
||||
)
|
||||
_print_report("overlapping", overlapping_report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Assembly and kinematic constraints for a four-planet planetary reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from dimensions import (
|
||||
FIXED_RING_REDUCTION,
|
||||
PLANET_COUNT,
|
||||
PLANET_PITCH_RADIUS,
|
||||
RING_PITCH_RADIUS,
|
||||
SUN_PITCH_RADIUS,
|
||||
)
|
||||
from materials import make_materials_rdict
|
||||
from parts import (
|
||||
make_carrier_rpart,
|
||||
make_planet_component_rplacement,
|
||||
make_planet_gear_rpart,
|
||||
make_ring_gear_rpart,
|
||||
make_sun_gear_rpart,
|
||||
)
|
||||
|
||||
|
||||
def make_four_planet_planetary_reducer_rassembly() -> scad.Assembly:
|
||||
"""Build and solve the exposed four-planet fixed-ring reducer gearset."""
|
||||
|
||||
print(
|
||||
f"ratio_plan: fixed_ring={FIXED_RING_REDUCTION:.3f}:1 "
|
||||
f"planets={PLANET_COUNT} sun_r={SUN_PITCH_RADIUS:.3f} "
|
||||
f"planet_r={PLANET_PITCH_RADIUS:.3f} ring_r={RING_PITCH_RADIUS:.3f}"
|
||||
)
|
||||
materials = make_materials_rdict()
|
||||
sun = make_sun_gear_rpart(material=materials["gear"])
|
||||
ring = make_ring_gear_rpart(material=materials["ring"])
|
||||
planet = make_planet_gear_rpart(material=materials["gear"])
|
||||
carrier = make_carrier_rpart(material=materials["carrier"])
|
||||
|
||||
reducer = scad.make_assembly_rassembly(
|
||||
assembly_id="four_planet_planetary_reducer",
|
||||
name="Exposed 3.5:1 four-planet fixed-ring planetary reducer gearset",
|
||||
)
|
||||
for component_id, item, placement, name in (
|
||||
("fixed_ring", ring, scad.identity_placement_rplacement(), "Fixed internal ring gear"),
|
||||
("sun_input", sun, scad.identity_placement_rplacement(), "Input sun gear"),
|
||||
("output_carrier", carrier, scad.identity_placement_rplacement(), "Four-pin output carrier"),
|
||||
):
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=name,
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
reducer = scad.add_component_rassembly(
|
||||
assembly=reducer,
|
||||
item=planet,
|
||||
component_id=f"planet_{index + 1}",
|
||||
placement=make_planet_component_rplacement(index=index),
|
||||
name=f"Planet gear {index + 1}",
|
||||
)
|
||||
|
||||
reducer = _add_public_connectors_rassembly(assembly=reducer)
|
||||
reducer = _add_kinematic_constraints_rassembly(assembly=reducer)
|
||||
reducer = scad.solve_assembly_constraints_rassembly(assembly=reducer, strict=True)
|
||||
_ground_constraint_report(assembly=reducer)
|
||||
print(
|
||||
f"planetary_components: count={len(reducer.component_ids())} "
|
||||
f"constraints={len(reducer.constraint_ids())}"
|
||||
)
|
||||
return reducer
|
||||
|
||||
|
||||
def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
forwarded = (
|
||||
("fixed_axis", "fixed_ring", "axis", "Fixed ring datum"),
|
||||
("input_axis", "sun_input", "axis", "Sun input datum"),
|
||||
("output_axis", "output_carrier", "output_axis", "Carrier output datum"),
|
||||
)
|
||||
for connector_id, component_id, source_connector_id, name in forwarded:
|
||||
assembly = scad.forward_connector_rassembly(
|
||||
assembly=assembly,
|
||||
connector_id=connector_id,
|
||||
source_component_id=component_id,
|
||||
source_connector_id=source_connector_id,
|
||||
name=name,
|
||||
)
|
||||
print("public_connectors: " + ",".join(connector_id for connector_id, *_ in forwarded))
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_kinematic_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="fixed_ring")
|
||||
revolutes = (
|
||||
("sun_input_revolute", "fixed_ring", "axis", "sun_input", "axis", 0.0),
|
||||
("carrier_output_revolute", "fixed_ring", "axis", "output_carrier", "axis", 0.0),
|
||||
)
|
||||
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=_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=_ref(component_id=b_component, connector_id=b_connector),
|
||||
drive_angle_degrees=drive_angle,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_id = f"planet_{index + 1}"
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"planet_{index + 1}_pin_revolute",
|
||||
connector_a=_ref(component_id="output_carrier", connector_id=f"planet_{index + 1}_axis"),
|
||||
connector_b=_ref(component_id=planet_id, connector_id="axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"Planet {index + 1} pin bearing revolute",
|
||||
)
|
||||
assembly = scad.add_gear_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"sun_to_planet_{index + 1}_external_mesh",
|
||||
connector_a=_ref(component_id="sun_input", connector_id="axis"),
|
||||
connector_b=_ref(component_id=planet_id, connector_id="axis"),
|
||||
pitch_radius_a=SUN_PITCH_RADIUS,
|
||||
pitch_radius_b=PLANET_PITCH_RADIUS,
|
||||
phase_offset=None,
|
||||
name=f"Sun external mesh to planet {index + 1}",
|
||||
)
|
||||
assembly = scad.add_belt_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"ring_to_planet_{index + 1}_internal_mesh",
|
||||
connector_a=_ref(component_id="fixed_ring", connector_id="axis"),
|
||||
connector_b=_ref(component_id=planet_id, connector_id="axis"),
|
||||
pulley_radius_a=RING_PITCH_RADIUS,
|
||||
pulley_radius_b=PLANET_PITCH_RADIUS,
|
||||
phase_offset=None,
|
||||
name=f"Fixed ring internal mesh to planet {index + 1}",
|
||||
)
|
||||
print(
|
||||
f"constraints_added: grounded=1 revolute={2 + PLANET_COUNT} "
|
||||
f"external_mesh={PLANET_COUNT} internal_mesh={PLANET_COUNT}"
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
return scad.make_connector_ref_rconnectorref(
|
||||
component_id=component_id,
|
||||
connector_id=connector_id,
|
||||
)
|
||||
|
||||
|
||||
def _ground_constraint_report(*, assembly: scad.Assembly) -> None:
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
|
||||
print(
|
||||
f"assembly_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"constraint_{residual.constraint_id}: translation={residual.translation_error:.6g} "
|
||||
f"angle={residual.angular_error_degrees:.6g} ok={residual.within_tolerance}"
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Shared helpers for Example 19."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
def make_z_rotation_rplacement(
|
||||
*,
|
||||
origin: tuple[float, float, float],
|
||||
angle_degrees: float,
|
||||
) -> scad.Placement:
|
||||
"""Create a placement rotated about local Z and translated to origin."""
|
||||
|
||||
angle = math.radians(angle_degrees)
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(math.cos(angle), math.sin(angle), 0.0),
|
||||
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
|
||||
)
|
||||
|
||||
|
||||
def add_axis_connector_rpart(
|
||||
*,
|
||||
part: scad.Part,
|
||||
connector_id: str,
|
||||
origin: tuple[float, float, float],
|
||||
name: str,
|
||||
) -> scad.Part:
|
||||
"""Attach a topology-free Z-axis datum connector to a part."""
|
||||
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=connector_id,
|
||||
placement=scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(1.0, 0.0, 0.0),
|
||||
y_axis=(0.0, 1.0, 0.0),
|
||||
),
|
||||
name=name,
|
||||
)
|
||||
return scad.add_connector_rpart(part=part, connector=connector)
|
||||
|
||||
|
||||
def make_axis_part_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
body: scad.Solid,
|
||||
name: str,
|
||||
material: scad.Material,
|
||||
connector_specs: tuple[tuple[str, tuple[float, float, float], str], ...],
|
||||
) -> scad.Part:
|
||||
"""Wrap one solid as a part and attach named axis 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, connector_name in connector_specs:
|
||||
part = add_axis_connector_rpart(
|
||||
part=part,
|
||||
connector_id=connector_id,
|
||||
origin=origin,
|
||||
name=connector_name,
|
||||
)
|
||||
print(f"part_{part_id}: connectors={len(part.connectors)} volume={body.get_volume():.3f}")
|
||||
return part
|
||||
|
||||
|
||||
def ground_solid(*, label: str, solid: scad.Solid) -> None:
|
||||
"""Print a small QL-grounded summary for a generated solid."""
|
||||
|
||||
faces = ql.select(items=solid.get_faces()).all()
|
||||
edges = ql.select(items=solid.get_edges()).all()
|
||||
print(
|
||||
f"{label}: faces={len(faces)} edges={len(edges)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Design constants for the four-planet single-stage planetary reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
|
||||
MODULE = 1.5
|
||||
PRESSURE_ANGLE = 20.0
|
||||
GEAR_HEIGHT = 8.0
|
||||
BACKLASH = 0.04
|
||||
ADDENDUM_FACTOR = 1.0
|
||||
CLEARANCE_FACTOR = 0.25
|
||||
|
||||
SUN_TEETH = 24
|
||||
PLANET_TEETH = 18
|
||||
PLANET_COUNT = 4
|
||||
RING_TEETH = SUN_TEETH + 2 * PLANET_TEETH
|
||||
|
||||
SUN_PITCH_RADIUS = MODULE * SUN_TEETH / 2.0
|
||||
PLANET_PITCH_RADIUS = MODULE * PLANET_TEETH / 2.0
|
||||
RING_PITCH_RADIUS = MODULE * RING_TEETH / 2.0
|
||||
PLANET_CENTER_RADIUS = SUN_PITCH_RADIUS + PLANET_PITCH_RADIUS
|
||||
FIXED_RING_REDUCTION = 1.0 + RING_TEETH / SUN_TEETH
|
||||
|
||||
RING_RIM_THICKNESS = 4.0
|
||||
GEAR_AXIS_Z = GEAR_HEIGHT / 2.0
|
||||
|
||||
SUN_BORE_RADIUS = 3.0
|
||||
PLANET_PIN_RADIUS = 2.6
|
||||
PLANET_PIN_CLEARANCE_RADIUS = 3.2
|
||||
|
||||
CARRIER_BOTTOM_Z = -5.0
|
||||
CARRIER_THICKNESS = 4.0
|
||||
CARRIER_HUB_RADIUS = 10.0
|
||||
CARRIER_ARM_WIDTH = 6.0
|
||||
CARRIER_PIN_BOSS_RADIUS = 5.2
|
||||
CARRIER_PIN_HEIGHT = GEAR_HEIGHT + 6.0
|
||||
|
||||
|
||||
def planet_angle_degrees(*, index: int) -> float:
|
||||
"""Return the equally spaced carrier angle for one planet index."""
|
||||
|
||||
return 360.0 * index / PLANET_COUNT
|
||||
|
||||
|
||||
def planet_center_xy(*, index: int) -> tuple[float, float]:
|
||||
"""Return the XY pitch-center location for one planet."""
|
||||
|
||||
angle = math.radians(planet_angle_degrees(index=index))
|
||||
return (
|
||||
PLANET_CENTER_RADIUS * math.cos(angle),
|
||||
PLANET_CENTER_RADIUS * math.sin(angle),
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Build, solve, and export the four-planet planetary reducer gearset."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from assembly import make_four_planet_planetary_reducer_rassembly
|
||||
|
||||
|
||||
OUT_DIR = Path("examples/out/19_four_planet_planetary_reducer")
|
||||
|
||||
|
||||
def _build_four_planet_reducer():
|
||||
with scad.GraphSession(graph_id="four_planet_planetary_reducer") as session:
|
||||
assembly = make_four_planet_planetary_reducer_rassembly()
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
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 / "four_planet_planetary_reducer.model.json"
|
||||
session_path = OUT_DIR / "four_planet_planetary_reducer.session.json"
|
||||
step_path = OUT_DIR / "four_planet_planetary_reducer.step"
|
||||
fcstd_path = OUT_DIR / "four_planet_planetary_reducer.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json, session_json = _build_four_planet_reducer()
|
||||
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="FourPlanetPlanetaryReducer",
|
||||
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()
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Materials for the four-planet planetary reducer example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def make_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create simple material definitions for the exposed gearset."""
|
||||
|
||||
materials = {
|
||||
"gear": scad.make_material_rmaterial(
|
||||
material_id="case_hardened_steel",
|
||||
name="Case hardened gear steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.70, 0.70, 0.74),
|
||||
),
|
||||
"ring": scad.make_material_rmaterial(
|
||||
material_id="nitrided_internal_ring_steel",
|
||||
name="Nitrided internal ring steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.44, 0.46, 0.50),
|
||||
),
|
||||
"carrier": scad.make_material_rmaterial(
|
||||
material_id="aluminum_7075_t6",
|
||||
name="7075-T6 aluminum carrier",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.14, 0.48, 0.70),
|
||||
),
|
||||
}
|
||||
print("materials: " + ",".join(sorted(materials)))
|
||||
return materials
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Gear and carrier parts for the four-planet planetary reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from common import ground_solid, make_axis_part_rpart, make_z_rotation_rplacement
|
||||
from dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CARRIER_ARM_WIDTH,
|
||||
CARRIER_BOTTOM_Z,
|
||||
CARRIER_HUB_RADIUS,
|
||||
CARRIER_PIN_BOSS_RADIUS,
|
||||
CARRIER_PIN_HEIGHT,
|
||||
CARRIER_THICKNESS,
|
||||
CLEARANCE_FACTOR,
|
||||
GEAR_AXIS_Z,
|
||||
GEAR_HEIGHT,
|
||||
MODULE,
|
||||
PLANET_CENTER_RADIUS,
|
||||
PLANET_COUNT,
|
||||
PLANET_PIN_CLEARANCE_RADIUS,
|
||||
PLANET_PIN_RADIUS,
|
||||
PLANET_TEETH,
|
||||
PRESSURE_ANGLE,
|
||||
RING_RIM_THICKNESS,
|
||||
RING_TEETH,
|
||||
SUN_BORE_RADIUS,
|
||||
SUN_TEETH,
|
||||
planet_angle_degrees,
|
||||
planet_center_xy,
|
||||
)
|
||||
|
||||
|
||||
def make_sun_gear_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the input sun gear with a service bore and axis connector."""
|
||||
|
||||
sun = scad.std.gear.make_spur_gear_rsolid(
|
||||
n_teeth=SUN_TEETH,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=SUN_BORE_RADIUS,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
sun = scad.cut_rsolid(sun, bore, skip_non_intersecting=False)
|
||||
sun = scad.apply_tag(shape=sun, tag="role.sun_input_gear")
|
||||
ground_solid(label="sun_gear", solid=sun)
|
||||
return make_axis_part_rpart(
|
||||
part_id="sun_input_gear",
|
||||
body=sun,
|
||||
name="Input sun gear, 24 teeth",
|
||||
material=material,
|
||||
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Sun input axis"),),
|
||||
)
|
||||
|
||||
|
||||
def make_ring_gear_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the fixed internal ring gear without an enclosing housing."""
|
||||
|
||||
ring = scad.std.gear.make_spur_ring_gear_rsolid(
|
||||
n_teeth=RING_TEETH,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RING_RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
)
|
||||
ring = scad.apply_tag(shape=ring, tag="role.fixed_internal_ring_gear")
|
||||
ground_solid(label="ring_gear", solid=ring)
|
||||
return make_axis_part_rpart(
|
||||
part_id="fixed_ring_gear",
|
||||
body=ring,
|
||||
name="Fixed internal ring gear, 60 teeth",
|
||||
material=material,
|
||||
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Fixed ring axis"),),
|
||||
)
|
||||
|
||||
|
||||
def make_planet_gear_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create one reusable planet gear with a carrier-pin bore."""
|
||||
|
||||
planet = scad.std.gear.make_spur_gear_rsolid(
|
||||
n_teeth=PLANET_TEETH,
|
||||
module=MODULE,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=PLANET_PIN_CLEARANCE_RADIUS,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
planet = scad.cut_rsolid(planet, bore, skip_non_intersecting=False)
|
||||
planet = scad.apply_tag(shape=planet, tag="role.reusable_planet_gear")
|
||||
ground_solid(label="planet_gear", solid=planet)
|
||||
return make_axis_part_rpart(
|
||||
part_id="planet_gear",
|
||||
body=planet,
|
||||
name="Reusable planet gear, 18 teeth",
|
||||
material=material,
|
||||
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Planet spin axis"),),
|
||||
)
|
||||
|
||||
|
||||
def make_carrier_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the four-pin carrier output spider."""
|
||||
|
||||
hub = scad.make_cylinder_rsolid(
|
||||
radius=CARRIER_HUB_RADIUS,
|
||||
height=CARRIER_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, CARRIER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
arms: list[scad.Solid] = []
|
||||
pin_bosses: list[scad.Solid] = []
|
||||
pins: list[scad.Solid] = []
|
||||
for index in range(PLANET_COUNT):
|
||||
angle = planet_angle_degrees(index=index)
|
||||
x, y = planet_center_xy(index=index)
|
||||
arm = scad.make_box_rsolid(
|
||||
width=PLANET_CENTER_RADIUS + CARRIER_PIN_BOSS_RADIUS,
|
||||
height=CARRIER_ARM_WIDTH,
|
||||
depth=CARRIER_THICKNESS,
|
||||
bottom_face_center=(PLANET_CENTER_RADIUS / 2.0, 0.0, CARRIER_BOTTOM_Z),
|
||||
)
|
||||
arms.append(
|
||||
scad.rotate_shape(
|
||||
shape=arm,
|
||||
angle=angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
pin_bosses.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=CARRIER_PIN_BOSS_RADIUS,
|
||||
height=CARRIER_THICKNESS,
|
||||
bottom_face_center=(x, y, CARRIER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
pins.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=PLANET_PIN_RADIUS,
|
||||
height=CARRIER_PIN_HEIGHT,
|
||||
bottom_face_center=(x, y, CARRIER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
carrier = scad.union_rsolid([hub, arms, pin_bosses, pins], glue=False)
|
||||
center_bore = scad.make_cylinder_rsolid(
|
||||
radius=SUN_BORE_RADIUS + 0.8,
|
||||
height=CARRIER_THICKNESS + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, CARRIER_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
carrier = scad.cut_rsolid(carrier, center_bore, skip_non_intersecting=False)
|
||||
carrier = scad.apply_tag(shape=carrier, tag="role.four_pin_output_carrier")
|
||||
ground_solid(label="carrier", solid=carrier)
|
||||
connector_specs = [
|
||||
("axis", (0.0, 0.0, GEAR_AXIS_Z), "Carrier output axis"),
|
||||
("output_axis", (0.0, 0.0, GEAR_AXIS_Z), "Public output axis"),
|
||||
]
|
||||
connector_specs.extend(
|
||||
(
|
||||
f"planet_{index + 1}_axis",
|
||||
(*planet_center_xy(index=index), GEAR_AXIS_Z),
|
||||
f"Planet {index + 1} carrier pin axis",
|
||||
)
|
||||
for index in range(PLANET_COUNT)
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="four_pin_output_carrier",
|
||||
body=carrier,
|
||||
name="Four-pin output carrier spider",
|
||||
material=material,
|
||||
connector_specs=tuple(connector_specs),
|
||||
)
|
||||
|
||||
|
||||
def make_planet_component_rplacement(*, index: int) -> scad.Placement:
|
||||
"""Return the placement for one of the four equally spaced planets."""
|
||||
|
||||
angle = planet_angle_degrees(index=index)
|
||||
x, y = planet_center_xy(index=index)
|
||||
tooth_phase = angle + 180.0 - (180.0 / PLANET_TEETH)
|
||||
print(
|
||||
f"planet_{index + 1}: center=({x:.3f},{y:.3f},0.000) "
|
||||
f"carrier_angle={angle:.1f} spin_phase={tooth_phase:.1f}"
|
||||
)
|
||||
return make_z_rotation_rplacement(origin=(x, y, 0.0), angle_degrees=tooth_phase)
|
||||
@@ -0,0 +1,139 @@
|
||||
# Case 20: Integrated 50 mm BLDC Joint Actuator
|
||||
|
||||
## Classification
|
||||
|
||||
Product-level kinematic assembly. The actuator contains separately manufactured
|
||||
electrical, magnetic, bearing, gear, housing, and output parts. The fixed housing
|
||||
is the ground; the rotor, two carriers, and six planets are rotating components.
|
||||
|
||||
## Design Intent
|
||||
|
||||
- Fit a real inner-rotor brushless motor, a 20:1 reducer, a circular controller
|
||||
PCB, and serviceable wiring terminals into one coaxial 50 mm package.
|
||||
- Eliminate the separate motor-to-reducer coupler. The rotor shaft and stage-1
|
||||
sun are one steel solid.
|
||||
- Preserve a short load path from the output flange through two adjacent output
|
||||
bearings into a separate front bearing cap.
|
||||
- Keep the stator and both ring gears as replaceable press-fit inserts instead of
|
||||
hiding them inside an impossible one-piece enclosure.
|
||||
- Reserve rear-facing connector apertures and PCB mounting holes so electronics
|
||||
are not represented by an empty cosmetic volume.
|
||||
|
||||
## Envelope And Performance
|
||||
|
||||
| Item | Value |
|
||||
|---|---:|
|
||||
| Motor / housing outside diameter | 50.0 mm |
|
||||
| Structural axial envelope | 75.8 mm |
|
||||
| Terminal protrusion included | 77.3 mm |
|
||||
| Motor topology | 12-slot / 14-pole inner rotor |
|
||||
| Stator active length | 16.0 mm |
|
||||
| Rotor magnetic length | 17.5 mm |
|
||||
| Radial air gap | 0.30 mm |
|
||||
| Stage 1 | 15/15/45 teeth, 4:1 |
|
||||
| Stage 2 | 18/27/72 teeth, 5:1 |
|
||||
| Total reduction | 20:1 |
|
||||
| Reducer housing minimum cylindrical wall | 2.20 mm |
|
||||
|
||||
The two stages intentionally use different modules. Stage 1 uses module 0.80 to
|
||||
leave enough root section around the 8 mm direct-drive shaft. Stage 2 uses module
|
||||
0.55 to fit the 72-tooth, 5:1 ring inside the 50 mm housing. Both tooth sets obey
|
||||
the three-planet equal-spacing condition `(sun teeth + ring teeth) mod 3 = 0`.
|
||||
|
||||
## Bill Of Materials
|
||||
|
||||
| Part / subassembly | Manufacturing intent | Material / connection |
|
||||
|---|---|---|
|
||||
| Main reducer housing | One machined fixed part | 6061-T6 aluminum |
|
||||
| Motor shell | One machined fixed part | 6061-T6, six M3 front screws |
|
||||
| Rear bearing spider | Separate machined part | 7075-T6, four M2.5 screws |
|
||||
| Rear electronics cover | Separate machined part with PCB bosses | 6061-T6, four M2.5 screws |
|
||||
| Output bearing cap | Separate machined part | 7075-T6, six M3 screws |
|
||||
| Stator core | 12-slot laminated stack | Electrical steel, thermal press fit |
|
||||
| Windings | Twelve separately represented coil packs | Copper, varnish/potting retained |
|
||||
| Rotor shaft + stage-1 sun | One integrated machined solid | Hardened alloy steel |
|
||||
| Rotor magnets | Fourteen bonded inserts | NdFeB |
|
||||
| PCB | Circular controller board with real holes/notches | FR-4/copper |
|
||||
| Phase terminal | Three-position rear-access terminal | High-temperature polymer |
|
||||
| Power/CAN terminal | Four-position rear-access terminal | High-temperature polymer |
|
||||
| Fixed ring gears | Two replaceable press-fit inserts | Case-hardened steel |
|
||||
| Planet gears | Six gears with standard bearing seats | Case-hardened steel |
|
||||
| Stage-1 carrier + stage-2 sun | One integrated interstage part | 7075 carrier / modeled as steel-duty part |
|
||||
| Output carrier + flange | One integrated output part | 7075-T6 aluminum |
|
||||
| Motor bearings | 8x16x5 and 8x19x6 | Standard ball bearings |
|
||||
| Interstage bearing | 5x10x3 | Thin radial ball bearing in fixed divider |
|
||||
| Planet bearings | Six 3x6x3 bearings | Standard ball bearings |
|
||||
| Output bearings | Two 16x24x5 bearings | Standard ball bearings |
|
||||
|
||||
Fasteners are represented by matching holes and documented interfaces rather
|
||||
than individual screw solids. This keeps the graph focused while retaining
|
||||
manufacturable attachment geometry.
|
||||
|
||||
## Interface Table
|
||||
|
||||
| Constraint / interface | Motion meaning | Real connection | Geometry and clearance |
|
||||
|---|---|---|---|
|
||||
| `motor_shell_to_reducer_housing` | Fixed | Six M3 screws | 43.0 mm PCD, 3.2 mm holes |
|
||||
| `rear_spider_to_motor_shell` | Fixed | Four M2.5 screws | 40.6 mm PCD, 2.7 mm holes; face-to-face column/spider joint |
|
||||
| `rear_cover_to_motor_shell` | Fixed | Four M2.5 screws | Shared rear columns and holes |
|
||||
| `stator_to_motor_shell` | Fixed | Thermal press fit + potting | Nominal line-to-line CAD fit; tolerance sets interference |
|
||||
| `electronics_to_rear_cover` | Fixed | Four M2 PCB screws | 33.0 mm PCD and integrated standoffs |
|
||||
| `rotor_revolute` | Motor input rotation | Front/rear radial bearings | 8 mm shaft, 0.30 mm magnetic air gap |
|
||||
| `stage1_ring_fixed` | Fixed ring | Interference fit and axial clamp | 0.04 mm diametral modeled interference |
|
||||
| `stage1_carrier_revolute` | First reduction output | 5x10x3 radial bearing | Integrated 5 mm stage-2 sun shaft |
|
||||
| `stage2_ring_fixed` | Fixed ring | Interference fit and axial clamp | 0.04 mm diametral modeled interference |
|
||||
| `output_carrier_revolute` | Joint output rotation | Paired output bearings | 16 mm shaft in two 16x24x5 bearings |
|
||||
| Planet revolutes | Planet spin | 3x6x3 bearings on 3 mm pins | 0.05 mm radial gear-seat clearance |
|
||||
| Output cap to housing | Fixed | Six M3 screws | 43.0 mm PCD, 3.2 mm holes |
|
||||
| Output link | External fixed attachment | Six M3 screws | 34.0 mm PCD tapped holes, Ø15.96 locating pilot |
|
||||
|
||||
## Electronics Packaging
|
||||
|
||||
The controller PCB is a 44.4 mm circular board behind the rear motor bearing. It
|
||||
has a 10 mm center service bore, four M2 mounting holes, four large edge notches
|
||||
for the rear structural columns, three phase-terminal pin holes, and four
|
||||
power/CAN pin holes. Two rear-cover apertures expose the terminal bodies without
|
||||
removing the controller. The board remains removable after the rear cover and
|
||||
terminal screws are released.
|
||||
|
||||
## Assembly Order
|
||||
|
||||
1. Press the stator stack into the motor shell and pot the twelve winding packs.
|
||||
2. Install the rear bearing into the four-arm spider and bolt the spider to the
|
||||
shell's rear columns.
|
||||
3. Insert the rotor/shaft from the reducer side and support it with the front
|
||||
motor bearing in the reducer bulkhead.
|
||||
4. Bolt the motor shell to the reducer housing with the six-hole front interface.
|
||||
5. Insert the first fixed ring and planetary stage, the 5x10x3 interstage
|
||||
bearing, then the second fixed ring and planetary stage from the open front.
|
||||
6. Install the paired output bearings in the removable output cap, then bolt the
|
||||
cap to the reducer housing.
|
||||
7. Install the PCB and terminals on the rear-cover standoffs, connect phases and
|
||||
sensors, and attach the rear cover.
|
||||
|
||||
This sequence avoids the trapped 43 mm ring-gear problem in Case 16: both ring
|
||||
inserts and carriers enter through the open reducer front before the bearing cap
|
||||
is installed.
|
||||
|
||||
## Strength And Thermal Notes
|
||||
|
||||
- The motor shell retains 1.80 mm radial wall around the stator and the reducer
|
||||
shell retains 2.20 mm around the steel ring inserts.
|
||||
- The 43 mm PCD case holes pass through 18.5 mm-radius end lands, retaining
|
||||
1.4 mm of continuous aluminum ligament on the bore side of each M3 clearance
|
||||
hole instead of clipping only the thin cylindrical shell.
|
||||
- The front reducer bulkhead is 8 mm long around the 19 mm motor bearing.
|
||||
- Two adjacent output bearings form a 10 mm stack with 5 mm center spacing to
|
||||
distribute overturning load.
|
||||
- The output flange leaves 4.15 mm radial ligament beyond the M3 tapped-hole edges.
|
||||
- The output face carries a 1.5 mm-high, Ø15.96 locating pilot. Mating links use
|
||||
the pilot for concentric location and six Ø3.3 clearance holes with Ø6 socket-
|
||||
head counterbores; the screws provide clamp load rather than radial location.
|
||||
- External robot structure clamps the continuous Ø50 reducer sleeve at the
|
||||
`case_clamp_axis` datum (`Z = 20.0`) instead of sharing the internal output-cap
|
||||
retention screws.
|
||||
- The stator yoke contacts the aluminum shell over its full active length for a
|
||||
direct thermal path; controller heat can flow through the rear standoffs and
|
||||
cover.
|
||||
- Detailed tooth stress, bearing life, winding thermal limits, rotor retention,
|
||||
and fastener preload still require engineering calculation and prototype test.
|
||||
@@ -0,0 +1 @@
|
||||
"""Reusable implementation package for Example 20."""
|
||||
@@ -0,0 +1,441 @@
|
||||
"""Top-level integrated BLDC motor, controller, reducer, and housing assembly."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
try:
|
||||
from .bearings import (
|
||||
make_coaxial_bearing_rplacement,
|
||||
make_main_bearing_rassembly,
|
||||
make_planet_bearing_rplacement,
|
||||
make_standard_planet_bearing_rassembly,
|
||||
)
|
||||
from .common import connector_ref, ground_constraint_report
|
||||
from .dimensions import (
|
||||
FRONT_MOTOR_BEARING,
|
||||
FRONT_MOTOR_BEARING_CENTER_Z,
|
||||
INTERSTAGE_BEARING,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
OUTPUT_BEARING,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
PLANET_BEARING,
|
||||
PLANET_COUNT,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
REAR_MOTOR_BEARING,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
TOTAL_REDUCTION,
|
||||
StageSpec,
|
||||
)
|
||||
from .electronics import make_integrated_controller_rassembly
|
||||
from .gears import (
|
||||
make_output_carrier_flange_rpart,
|
||||
make_planet_rplacement,
|
||||
make_stage1_carrier_sun_rpart,
|
||||
make_stage_planet_gear_rpart,
|
||||
make_stage_ring_gear_rpart,
|
||||
)
|
||||
from .housing import (
|
||||
make_motor_shell_rpart,
|
||||
make_output_bearing_cap_rpart,
|
||||
make_rear_bearing_spider_rpart,
|
||||
make_rear_electronics_cover_rpart,
|
||||
make_reducer_housing_rpart,
|
||||
)
|
||||
from .motor import make_bldc_rotor_rassembly, make_bldc_stator_rassembly
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
from bearings import (
|
||||
make_coaxial_bearing_rplacement,
|
||||
make_main_bearing_rassembly,
|
||||
make_planet_bearing_rplacement,
|
||||
make_standard_planet_bearing_rassembly,
|
||||
)
|
||||
from common import connector_ref, ground_constraint_report
|
||||
from dimensions import (
|
||||
FRONT_MOTOR_BEARING,
|
||||
FRONT_MOTOR_BEARING_CENTER_Z,
|
||||
INTERSTAGE_BEARING,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
OUTPUT_BEARING,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
PLANET_BEARING,
|
||||
PLANET_COUNT,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
REAR_MOTOR_BEARING,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
TOTAL_REDUCTION,
|
||||
StageSpec,
|
||||
)
|
||||
from electronics import make_integrated_controller_rassembly
|
||||
from gears import (
|
||||
make_output_carrier_flange_rpart,
|
||||
make_planet_rplacement,
|
||||
make_stage1_carrier_sun_rpart,
|
||||
make_stage_planet_gear_rpart,
|
||||
make_stage_ring_gear_rpart,
|
||||
)
|
||||
from housing import (
|
||||
make_motor_shell_rpart,
|
||||
make_output_bearing_cap_rpart,
|
||||
make_rear_bearing_spider_rpart,
|
||||
make_rear_electronics_cover_rpart,
|
||||
make_reducer_housing_rpart,
|
||||
)
|
||||
from motor import make_bldc_rotor_rassembly, make_bldc_stator_rassembly
|
||||
|
||||
|
||||
def make_integrated_bldc_joint_actuator_rassembly(
|
||||
*, materials: dict[str, scad.Material]
|
||||
) -> scad.Assembly:
|
||||
"""Build and solve the complete compact 50 mm joint actuator."""
|
||||
|
||||
component_specs = make_integrated_bldc_joint_actuator_components_rtuple(
|
||||
materials=materials
|
||||
)
|
||||
actuator = scad.make_assembly_rassembly(
|
||||
assembly_id="integrated_50mm_bldc_joint_actuator",
|
||||
name="50 mm 12-slot/14-pole BLDC joint actuator with 20:1 reducer and circular ESC",
|
||||
)
|
||||
for component_id, item, placement, name in component_specs:
|
||||
actuator = scad.add_component_rassembly(
|
||||
assembly=actuator,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=name,
|
||||
)
|
||||
|
||||
actuator = _add_public_connectors_rassembly(assembly=actuator)
|
||||
actuator = _add_constraints_rassembly(assembly=actuator)
|
||||
actuator = scad.solve_assembly_constraints_rassembly(assembly=actuator, strict=True)
|
||||
ground_constraint_report(label="actuator", assembly=actuator)
|
||||
return actuator
|
||||
|
||||
|
||||
def make_integrated_bldc_joint_actuator_components_rtuple(
|
||||
*, materials: dict[str, scad.Material]
|
||||
) -> tuple[tuple[str, scad.Part | scad.Assembly, scad.Placement, str], ...]:
|
||||
"""Build the actuator component inventory without creating a parent assembly."""
|
||||
|
||||
print(
|
||||
f"ratio_plan: stage1={STAGE_1.fixed_ring_ratio:.1f}:1 "
|
||||
f"stage2={STAGE_2.fixed_ring_ratio:.1f}:1 total={TOTAL_REDUCTION:.1f}:1"
|
||||
)
|
||||
reducer_housing = make_reducer_housing_rpart(material=materials["housing"])
|
||||
motor_shell = make_motor_shell_rpart(material=materials["housing"])
|
||||
rear_spider = make_rear_bearing_spider_rpart(material=materials["carrier"])
|
||||
rear_cover = make_rear_electronics_cover_rpart(material=materials["housing"])
|
||||
output_cap = make_output_bearing_cap_rpart(material=materials["carrier"])
|
||||
stator = make_bldc_stator_rassembly(
|
||||
steel_material=materials["electrical_steel"],
|
||||
copper_material=materials["copper"],
|
||||
)
|
||||
rotor = make_bldc_rotor_rassembly(
|
||||
steel_material=materials["gear"],
|
||||
magnet_material=materials["magnet"],
|
||||
)
|
||||
controller = make_integrated_controller_rassembly(
|
||||
pcb_material=materials["pcb"],
|
||||
terminal_material=materials["terminal"],
|
||||
)
|
||||
|
||||
stage1_ring = make_stage_ring_gear_rpart(stage=STAGE_1, material=materials["gear"])
|
||||
stage1_planet = make_stage_planet_gear_rpart(stage=STAGE_1, material=materials["gear"])
|
||||
stage1_carrier = make_stage1_carrier_sun_rpart(material=materials["gear"])
|
||||
stage2_ring = make_stage_ring_gear_rpart(stage=STAGE_2, material=materials["gear"])
|
||||
stage2_planet = make_stage_planet_gear_rpart(stage=STAGE_2, material=materials["gear"])
|
||||
output_carrier = make_output_carrier_flange_rpart(stage=STAGE_2, material=materials["carrier"])
|
||||
|
||||
rear_motor_bearing = make_main_bearing_rassembly(
|
||||
bearing_id="rear_motor_8x16x5",
|
||||
spec=REAR_MOTOR_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
front_motor_bearing = make_main_bearing_rassembly(
|
||||
bearing_id="front_motor_8x19x6",
|
||||
spec=FRONT_MOTOR_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
interstage_bearing = make_main_bearing_rassembly(
|
||||
bearing_id="interstage_5x10x3",
|
||||
spec=INTERSTAGE_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
planet_bearing = make_standard_planet_bearing_rassembly(
|
||||
bearing_id="planet_3x6x3",
|
||||
spec=PLANET_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
output_bearing = make_main_bearing_rassembly(
|
||||
bearing_id="output_16x24x5",
|
||||
spec=OUTPUT_BEARING,
|
||||
material=materials["gear"],
|
||||
)
|
||||
|
||||
fixed_components = (
|
||||
("reducer_housing", reducer_housing, scad.identity_placement_rplacement(), "Fixed reducer housing"),
|
||||
("motor_shell", motor_shell, scad.identity_placement_rplacement(), "Fixed BLDC shell"),
|
||||
("rear_bearing_spider", rear_spider, scad.identity_placement_rplacement(), "Rear motor-bearing spider"),
|
||||
("rear_electronics_cover", rear_cover, scad.identity_placement_rplacement(), "Rear controller cover"),
|
||||
("output_bearing_cap", output_cap, scad.identity_placement_rplacement(), "Output bearing cap"),
|
||||
("stator", stator, scad.identity_placement_rplacement(), "12-slot fixed stator"),
|
||||
("rotor", rotor, scad.identity_placement_rplacement(), "14-pole rotor and direct sun shaft"),
|
||||
("controller", controller, scad.identity_placement_rplacement(), "Circular integrated controller"),
|
||||
("stage1_carrier", stage1_carrier, scad.identity_placement_rplacement(), "Stage 1 carrier and stage 2 sun"),
|
||||
("output_carrier", output_carrier, scad.identity_placement_rplacement(), "Stage 2 carrier and output flange"),
|
||||
("stage1_ring", stage1_ring, _stage_rplacement(stage=STAGE_1), "Stage 1 fixed ring insert"),
|
||||
("stage2_ring", stage2_ring, _stage_rplacement(stage=STAGE_2), "Stage 2 fixed ring insert"),
|
||||
)
|
||||
print(f"actuator_base_components: count={len(fixed_components)}")
|
||||
|
||||
planet_components = []
|
||||
for stage, planet in ((STAGE_1, stage1_planet), (STAGE_2, stage2_planet)):
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_components.append(
|
||||
(
|
||||
f"{stage.stage_id}_planet_{index + 1}",
|
||||
planet,
|
||||
make_planet_rplacement(stage=stage, index=index),
|
||||
f"{stage.label} planet {index + 1}",
|
||||
)
|
||||
)
|
||||
|
||||
bearing_components = (
|
||||
(
|
||||
"rear_motor_bearing",
|
||||
rear_motor_bearing,
|
||||
make_coaxial_bearing_rplacement(center_z=REAR_BEARING_CENTER_Z),
|
||||
"Rear rotor bearing",
|
||||
),
|
||||
(
|
||||
"front_motor_bearing",
|
||||
front_motor_bearing,
|
||||
make_coaxial_bearing_rplacement(center_z=FRONT_MOTOR_BEARING_CENTER_Z),
|
||||
"Front rotor bearing",
|
||||
),
|
||||
(
|
||||
"interstage_bearing",
|
||||
interstage_bearing,
|
||||
make_coaxial_bearing_rplacement(center_z=INTERSTAGE_BEARING_CENTER_Z),
|
||||
"Stage 1 carrier support bearing",
|
||||
),
|
||||
(
|
||||
"output_bearing_1",
|
||||
output_bearing,
|
||||
make_coaxial_bearing_rplacement(center_z=OUTPUT_BEARING_1_CENTER_Z),
|
||||
"Rear output bearing",
|
||||
),
|
||||
(
|
||||
"output_bearing_2",
|
||||
output_bearing,
|
||||
make_coaxial_bearing_rplacement(center_z=OUTPUT_BEARING_2_CENTER_Z),
|
||||
"Front output bearing",
|
||||
),
|
||||
)
|
||||
planet_bearing_components = []
|
||||
for stage in (STAGE_1, STAGE_2):
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_bearing_components.append(
|
||||
(
|
||||
f"{stage.stage_id}_planet_bearing_{index + 1}",
|
||||
planet_bearing,
|
||||
make_planet_bearing_rplacement(stage=stage, index=index),
|
||||
f"{stage.label} planet bearing {index + 1}",
|
||||
)
|
||||
)
|
||||
print(f"bearing_components: motor=2 interstage=1 output=2 planet={PLANET_COUNT * 2}")
|
||||
return tuple(
|
||||
[
|
||||
*fixed_components,
|
||||
*planet_components,
|
||||
*bearing_components,
|
||||
*planet_bearing_components,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
forwarded = (
|
||||
("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_link_axis", "output_carrier", "output_link_axis", "Rotating six-hole output flange"),
|
||||
("phase_terminal_access", "controller", "phase_access", "Rear phase-terminal service datum"),
|
||||
("power_can_terminal_access", "controller", "power_can_access", "Rear power/CAN service datum"),
|
||||
)
|
||||
for connector_id, source_component_id, source_connector_id, name in forwarded:
|
||||
assembly = scad.forward_connector_rassembly(
|
||||
assembly=assembly,
|
||||
connector_id=connector_id,
|
||||
source_component_id=source_component_id,
|
||||
source_connector_id=source_connector_id,
|
||||
name=name,
|
||||
offset=None,
|
||||
)
|
||||
print("actuator_public_connectors: " + ",".join(item[0] for item in forwarded))
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="reducer_housing")
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage1_ring")
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="stage2_ring")
|
||||
fixed_pairs = (
|
||||
("motor_shell_to_reducer_housing", "reducer_housing", "motor_mount_axis", "motor_shell", "reducer_mount_axis"),
|
||||
("rear_spider_to_motor_shell", "motor_shell", "rear_spider_axis", "rear_bearing_spider", "shell_axis"),
|
||||
("rear_cover_to_motor_shell", "motor_shell", "rear_cover_axis", "rear_electronics_cover", "shell_axis"),
|
||||
("stator_to_motor_shell", "motor_shell", "stator_axis", "stator", "shell_axis"),
|
||||
("controller_to_rear_cover", "rear_electronics_cover", "pcb_axis", "controller", "cover_axis"),
|
||||
("stage1_ring_fixed", "reducer_housing", "stage1_ring_axis", "stage1_ring", "axis"),
|
||||
("stage2_ring_fixed", "reducer_housing", "stage2_ring_axis", "stage2_ring", "axis"),
|
||||
("output_cap_to_reducer_housing", "reducer_housing", "output_cap_axis", "output_bearing_cap", "housing_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("_", " "),
|
||||
)
|
||||
|
||||
primary_revolutes = (
|
||||
("rotor_revolute", "reducer_housing", "front_motor_bearing_axis", "rotor", "front_bearing_axis"),
|
||||
("stage1_carrier_revolute", "reducer_housing", "stage1_carrier_axis", "stage1_carrier", "carrier_axis"),
|
||||
("output_carrier_revolute", "reducer_housing", "stage2_carrier_axis", "output_carrier", "carrier_axis"),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, b_component, b_connector in primary_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=0.0,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
assembly = _add_stage_constraints_rassembly(
|
||||
assembly=assembly,
|
||||
stage=STAGE_1,
|
||||
sun_component="rotor",
|
||||
sun_connector="front_bearing_axis",
|
||||
ring_component="stage1_ring",
|
||||
carrier_component="stage1_carrier",
|
||||
)
|
||||
assembly = _add_stage_constraints_rassembly(
|
||||
assembly=assembly,
|
||||
stage=STAGE_2,
|
||||
sun_component="stage1_carrier",
|
||||
sun_connector="carrier_axis",
|
||||
ring_component="stage2_ring",
|
||||
carrier_component="output_carrier",
|
||||
)
|
||||
assembly = _add_bearing_constraints_rassembly(assembly=assembly)
|
||||
print("actuator_constraints: fixed=8 primary_revolute=3 planet_revolute=6 gear=6 internal=6 bearing_interfaces=22")
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_stage_constraints_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
stage: StageSpec,
|
||||
sun_component: str,
|
||||
sun_connector: str,
|
||||
ring_component: str,
|
||||
carrier_component: str,
|
||||
) -> scad.Assembly:
|
||||
for index in range(PLANET_COUNT):
|
||||
planet_component = f"{stage.stage_id}_planet_{index + 1}"
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{planet_component}_revolute",
|
||||
connector_a=connector_ref(component_id=carrier_component, connector_id=f"planet_{index + 1}_axis"),
|
||||
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{stage.label} planet {index + 1} bearing axis",
|
||||
)
|
||||
assembly = scad.add_gear_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{stage.stage_id}_sun_planet_{index + 1}_mesh",
|
||||
connector_a=connector_ref(component_id=sun_component, connector_id=sun_connector),
|
||||
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
|
||||
pitch_radius_a=stage.sun_pitch_radius,
|
||||
pitch_radius_b=stage.planet_pitch_radius,
|
||||
phase_offset=None,
|
||||
name=f"{stage.label} sun to planet {index + 1} external mesh",
|
||||
)
|
||||
assembly = scad.add_belt_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{stage.stage_id}_ring_planet_{index + 1}_internal_mesh",
|
||||
connector_a=connector_ref(component_id=ring_component, connector_id="axis"),
|
||||
connector_b=connector_ref(component_id=planet_component, connector_id="axis"),
|
||||
pulley_radius_a=stage.ring_pitch_radius,
|
||||
pulley_radius_b=stage.planet_pitch_radius,
|
||||
phase_offset=None,
|
||||
name=f"{stage.label} fixed-ring to planet {index + 1} internal mesh",
|
||||
)
|
||||
print(
|
||||
f"{stage.stage_id}_mesh: sun_r={stage.sun_pitch_radius:.3f} "
|
||||
f"planet_r={stage.planet_pitch_radius:.3f} center={stage.planet_center_radius:.3f}"
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _add_bearing_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
interfaces = (
|
||||
("rear_bearing_outer_to_spider", "rear_bearing_spider", "bearing_axis", "rear_motor_bearing", "outer_axis"),
|
||||
("rear_bearing_inner_to_rotor", "rotor", "rear_bearing_axis", "rear_motor_bearing", "inner_axis"),
|
||||
("front_bearing_outer_to_housing", "reducer_housing", "front_motor_bearing_axis", "front_motor_bearing", "outer_axis"),
|
||||
("front_bearing_inner_to_rotor", "rotor", "front_bearing_axis", "front_motor_bearing", "inner_axis"),
|
||||
("interstage_bearing_outer_to_housing", "reducer_housing", "interstage_bearing_axis", "interstage_bearing", "outer_axis"),
|
||||
("interstage_bearing_inner_to_carrier", "stage1_carrier", "interstage_bearing_axis", "interstage_bearing", "inner_axis"),
|
||||
("output_bearing_1_outer_to_cap", "output_bearing_cap", "bearing_1_axis", "output_bearing_1", "outer_axis"),
|
||||
("output_bearing_1_inner_to_carrier", "output_carrier", "bearing_1_axis", "output_bearing_1", "inner_axis"),
|
||||
("output_bearing_2_outer_to_cap", "output_bearing_cap", "bearing_2_axis", "output_bearing_2", "outer_axis"),
|
||||
("output_bearing_2_inner_to_carrier", "output_carrier", "bearing_2_axis", "output_bearing_2", "inner_axis"),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, b_component, b_connector in interfaces:
|
||||
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=None,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
for stage, carrier_component in ((STAGE_1, "stage1_carrier"), (STAGE_2, "output_carrier")):
|
||||
for index in range(PLANET_COUNT):
|
||||
planet = f"{stage.stage_id}_planet_{index + 1}"
|
||||
bearing = f"{stage.stage_id}_planet_bearing_{index + 1}"
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{bearing}_outer_to_planet",
|
||||
connector_a=connector_ref(component_id=planet, connector_id="bearing_axis"),
|
||||
connector_b=connector_ref(component_id=bearing, connector_id="outer_axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{stage.label} planet {index + 1} bearing outer-ring fit",
|
||||
)
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{bearing}_inner_to_pin",
|
||||
connector_a=connector_ref(
|
||||
component_id=carrier_component,
|
||||
connector_id=f"planet_{index + 1}_bearing_axis",
|
||||
),
|
||||
connector_b=connector_ref(component_id=bearing, connector_id="inner_axis"),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name=f"{stage.label} planet {index + 1} bearing inner-ring pin fit",
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _stage_rplacement(*, stage: StageSpec) -> scad.Placement:
|
||||
return scad.make_placement_rplacement(origin=(0.0, 0.0, stage.bottom_z))
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Standard bearing factories and coaxial/planet placements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
if __package__:
|
||||
from .common import make_annulus_rsolid, make_axis_part_rpart, make_z_rotation_rplacement
|
||||
from .dimensions import BearingSpec, PLANET_COUNT, StageSpec
|
||||
from .gears import planet_center_xy
|
||||
else:
|
||||
from common import make_annulus_rsolid, make_axis_part_rpart, make_z_rotation_rplacement
|
||||
from dimensions import BearingSpec, PLANET_COUNT, StageSpec
|
||||
from gears import planet_center_xy
|
||||
|
||||
|
||||
def make_standard_planet_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
spec: BearingSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Assembly:
|
||||
"""Create the reused catalog-style planet ball bearing assembly."""
|
||||
|
||||
bearing = make_main_bearing_rassembly(
|
||||
bearing_id=bearing_id,
|
||||
spec=spec,
|
||||
material=material,
|
||||
)
|
||||
print(
|
||||
f"bearing_{bearing_id}: bore={spec.bore_diameter:.1f} od={spec.outer_diameter:.1f} "
|
||||
f"width={spec.width:.1f} balls={spec.ball_count} material={material.material_id}"
|
||||
)
|
||||
return bearing
|
||||
|
||||
|
||||
def make_main_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
spec: BearingSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Assembly:
|
||||
"""Create a uniquely identified bearing when several catalog sizes share one graph."""
|
||||
|
||||
bore_radius = spec.bore_diameter / 2.0
|
||||
outer_radius = spec.outer_diameter / 2.0
|
||||
ball_radius = spec.ball_diameter / 2.0
|
||||
pitch_radius = (bore_radius + outer_radius) / 2.0
|
||||
inner_outer_radius = pitch_radius - ball_radius * 0.55
|
||||
outer_inner_radius = pitch_radius + ball_radius * 0.55
|
||||
inner_ring = make_annulus_rsolid(
|
||||
outer_radius=inner_outer_radius,
|
||||
inner_radius=bore_radius,
|
||||
bottom_z=-spec.width / 2.0,
|
||||
height=spec.width,
|
||||
tags=("role.bearing_inner_ring",),
|
||||
)
|
||||
outer_ring = make_annulus_rsolid(
|
||||
outer_radius=outer_radius,
|
||||
inner_radius=outer_inner_radius,
|
||||
bottom_z=-spec.width / 2.0,
|
||||
height=spec.width,
|
||||
tags=("role.bearing_outer_ring",),
|
||||
)
|
||||
ball = scad.make_sphere_rsolid(
|
||||
radius=ball_radius,
|
||||
center=(pitch_radius, 0.0, 0.0),
|
||||
)
|
||||
inner_part = make_axis_part_rpart(
|
||||
part_id=f"{bearing_id}_inner_ring",
|
||||
body=inner_ring,
|
||||
name=f"{bearing_id} inner race",
|
||||
material=material,
|
||||
connectors=(("axis", (0.0, 0.0, 0.0), "Inner-ring axis"),),
|
||||
)
|
||||
outer_part = make_axis_part_rpart(
|
||||
part_id=f"{bearing_id}_outer_ring",
|
||||
body=outer_ring,
|
||||
name=f"{bearing_id} outer race",
|
||||
material=material,
|
||||
connectors=(("axis", (0.0, 0.0, 0.0), "Outer-ring axis"),),
|
||||
)
|
||||
ball_part = make_axis_part_rpart(
|
||||
part_id=f"{bearing_id}_reusable_ball",
|
||||
body=ball,
|
||||
name=f"{bearing_id} reusable rolling element",
|
||||
material=material,
|
||||
connectors=(),
|
||||
)
|
||||
bearing = scad.make_assembly_rassembly(
|
||||
assembly_id=bearing_id,
|
||||
name=f"Ball bearing {spec.bore_diameter:g}x{spec.outer_diameter:g}x{spec.width:g}",
|
||||
)
|
||||
bearing = scad.add_component_rassembly(
|
||||
assembly=bearing,
|
||||
item=outer_part,
|
||||
component_id="outer_ring",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Outer ring",
|
||||
)
|
||||
bearing = scad.add_component_rassembly(
|
||||
assembly=bearing,
|
||||
item=inner_part,
|
||||
component_id="inner_ring",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Inner ring",
|
||||
)
|
||||
for index in range(spec.ball_count):
|
||||
angle = 360.0 * index / spec.ball_count
|
||||
bearing = scad.add_component_rassembly(
|
||||
assembly=bearing,
|
||||
item=ball_part,
|
||||
component_id=f"ball_{index + 1:02d}",
|
||||
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
|
||||
name=f"Rolling element {index + 1}",
|
||||
)
|
||||
bearing = scad.add_revolute_constraint_rassembly(
|
||||
assembly=bearing,
|
||||
constraint_id="inner_outer_revolute",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="outer_ring",
|
||||
connector_id="axis",
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="inner_ring",
|
||||
connector_id="axis",
|
||||
),
|
||||
drive_angle_degrees=None,
|
||||
angle_limit=None,
|
||||
name="Inner ring rotation in outer ring",
|
||||
)
|
||||
for connector_id, component_id in (("outer_axis", "outer_ring"), ("inner_axis", "inner_ring")):
|
||||
bearing = scad.forward_connector_rassembly(
|
||||
assembly=bearing,
|
||||
connector_id=connector_id,
|
||||
source_component_id=component_id,
|
||||
source_connector_id="axis",
|
||||
name=connector_id.replace("_", " "),
|
||||
offset=None,
|
||||
)
|
||||
radial_wall = (spec.outer_diameter - spec.bore_diameter) / 2.0 - spec.ball_diameter
|
||||
axial_margin = spec.width - spec.ball_diameter
|
||||
print(
|
||||
f"bearing_{bearing_id}: bore={spec.bore_diameter:.1f} od={spec.outer_diameter:.1f} "
|
||||
f"width={spec.width:.1f} balls={spec.ball_count} radial_wall={radial_wall:.2f} "
|
||||
f"axial_margin={axial_margin:.2f}"
|
||||
)
|
||||
return bearing
|
||||
|
||||
|
||||
def make_coaxial_bearing_rplacement(*, center_z: float) -> scad.Placement:
|
||||
"""Place a standard bearing center plane on the actuator Z axis."""
|
||||
|
||||
return make_z_rotation_rplacement(origin=(0.0, 0.0, center_z), angle_degrees=0.0)
|
||||
|
||||
|
||||
def make_planet_bearing_rplacement(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
index: int,
|
||||
) -> scad.Placement:
|
||||
"""Place a standard planet bearing at the gear midplane."""
|
||||
|
||||
if index < 0 or index >= PLANET_COUNT:
|
||||
raise ValueError(f"planet bearing index out of range: {index}")
|
||||
center = planet_center_xy(stage=stage, index=index)
|
||||
print(
|
||||
f"{stage.stage_id}_planet_bearing_{index + 1}: "
|
||||
f"center=({center[0]:.3f},{center[1]:.3f},{stage.mid_z:.3f})"
|
||||
)
|
||||
return make_z_rotation_rplacement(
|
||||
origin=(center[0], center[1], stage.mid_z),
|
||||
angle_degrees=0.0,
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Shared construction, tagging, connector, and grounding helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
def apply_tags(*, shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
|
||||
"""Apply semantic tags through the public functional API."""
|
||||
|
||||
tagged = shape
|
||||
for tag in tags:
|
||||
tagged = scad.apply_tag(shape=tagged, tag=tag)
|
||||
return tagged
|
||||
|
||||
|
||||
def make_annulus_rsolid(
|
||||
*,
|
||||
outer_radius: float,
|
||||
inner_radius: float,
|
||||
bottom_z: float,
|
||||
height: float,
|
||||
tags: Iterable[str],
|
||||
) -> scad.Solid:
|
||||
"""Create a strict single-solid annular cylinder."""
|
||||
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=outer_radius,
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=inner_radius,
|
||||
height=height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
annulus = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
return apply_tags(shape=annulus, tags=tags)
|
||||
|
||||
|
||||
def make_axis_part_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
body: scad.Solid,
|
||||
name: str,
|
||||
material: scad.Material,
|
||||
connectors: Iterable[tuple[str, tuple[float, float, float], str]],
|
||||
) -> scad.Part:
|
||||
"""Create a single-body part with stable placement-based axis datums."""
|
||||
|
||||
part = scad.make_part_rpart(part_id=part_id, body=body, name=name)
|
||||
part = scad.assign_material_rpart(part=part, material=material)
|
||||
connector_count = 0
|
||||
for connector_id, origin, connector_name in connectors:
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=connector_id,
|
||||
placement=scad.make_placement_rplacement(origin=origin),
|
||||
name=connector_name,
|
||||
)
|
||||
part = scad.add_connector_rpart(part=part, connector=connector)
|
||||
connector_count += 1
|
||||
ground_solid(label=part_id, solid=body)
|
||||
print(f"part_{part_id}: connectors={connector_count} material={material.material_id}")
|
||||
return part
|
||||
|
||||
|
||||
def make_z_rotation_rplacement(
|
||||
*,
|
||||
origin: tuple[float, float, float],
|
||||
angle_degrees: float,
|
||||
) -> scad.Placement:
|
||||
"""Return a right-handed placement rotated about Z."""
|
||||
|
||||
angle = math.radians(angle_degrees)
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(math.cos(angle), math.sin(angle), 0.0),
|
||||
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
|
||||
)
|
||||
|
||||
|
||||
def radial_centers(*, count: int, radius: float, angle_offset: float = 0.0):
|
||||
"""Yield index, angle in degrees, and XY center on a bolt/pole circle."""
|
||||
|
||||
for index in range(count):
|
||||
angle_degrees = angle_offset + 360.0 * index / count
|
||||
angle = math.radians(angle_degrees)
|
||||
yield index, angle_degrees, (radius * math.cos(angle), radius * math.sin(angle))
|
||||
|
||||
|
||||
def make_axial_hole_cutters_rsolids(
|
||||
*,
|
||||
count: int,
|
||||
pcd: float,
|
||||
hole_radius: float,
|
||||
bottom_z: float,
|
||||
height: float,
|
||||
angle_offset: float = 0.0,
|
||||
) -> list[scad.Solid]:
|
||||
"""Create equally spaced axial hole cutters."""
|
||||
|
||||
cutters = []
|
||||
for _index, _angle, center in radial_centers(
|
||||
count=count,
|
||||
radius=pcd / 2.0,
|
||||
angle_offset=angle_offset,
|
||||
):
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=hole_radius,
|
||||
height=height,
|
||||
bottom_face_center=(center[0], center[1], bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
|
||||
|
||||
def ground_solid(*, label: str, solid: scad.Solid) -> None:
|
||||
"""Print a concise QL-backed solid summary."""
|
||||
|
||||
faces = ql.select(items=solid.get_faces()).all()
|
||||
role_faces = ql.select(items=faces).where(ql.tag(pattern="role.*")).all()
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
|
||||
|
||||
def ground_compound(*, label: str, compound: scad.Compound) -> None:
|
||||
"""Print a concise QL-backed assembly projection summary."""
|
||||
|
||||
solids = ql.select(items=compound.get_solids()).all()
|
||||
faces = sum(len(ql.select(items=solid.get_faces()).all()) for solid in solids)
|
||||
volume = sum(solid.get_volume() for solid in solids)
|
||||
print(f"{label}: solids={len(solids)} faces={faces} volume={volume:.3f}")
|
||||
|
||||
|
||||
def connector_ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
"""Create a component-scoped connector reference."""
|
||||
|
||||
return scad.make_connector_ref_rconnectorref(
|
||||
component_id=component_id,
|
||||
connector_id=connector_id,
|
||||
)
|
||||
|
||||
|
||||
def ground_constraint_report(*, label: str, assembly: scad.Assembly) -> None:
|
||||
"""Print solved state and only non-zero residual facts."""
|
||||
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
|
||||
worst_translation = max((item.translation_error for item in report.residuals), default=0.0)
|
||||
worst_angle = max((item.angular_error_degrees for item in report.residuals), default=0.0)
|
||||
print(
|
||||
f"{label}_constraints: solved={report.solved} components={len(assembly.component_ids())} "
|
||||
f"constraints={len(assembly.constraint_ids())} unsolved={len(report.unsolved_component_ids)} "
|
||||
f"max_translation={worst_translation:.6g} max_angle={worst_angle:.6g}"
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Design constants for the integrated 50 mm BLDC joint actuator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
PACKAGE_RADIUS = 25.0
|
||||
PACKAGE_STRUCTURAL_BOTTOM_Z = -37.0
|
||||
PACKAGE_TOP_Z = 40.3
|
||||
|
||||
MOTOR_SLOT_COUNT = 12
|
||||
MOTOR_POLE_COUNT = 14
|
||||
MOTOR_SHELL_INNER_RADIUS = 23.20
|
||||
MOTOR_SHELL_BOTTOM_Z = -34.0
|
||||
MOTOR_SHELL_TOP_Z = -6.0
|
||||
MOTOR_STATOR_BOTTOM_Z = -24.5
|
||||
MOTOR_STATOR_TOP_Z = -8.5
|
||||
MOTOR_STATOR_OUTER_RADIUS = 23.20
|
||||
MOTOR_STATOR_YOKE_INNER_RADIUS = 20.20
|
||||
MOTOR_STATOR_TOOTH_INNER_RADIUS = 15.00
|
||||
MOTOR_STATOR_TOOTH_WIDTH = 2.60
|
||||
MOTOR_ROTOR_BOTTOM_Z = -25.0
|
||||
MOTOR_ROTOR_TOP_Z = -7.5
|
||||
MOTOR_ROTOR_BACKIRON_RADIUS = 13.50
|
||||
MOTOR_MAGNET_OUTER_RADIUS = 14.70
|
||||
MOTOR_MAGNET_TANGENTIAL_WIDTH = 5.0
|
||||
MOTOR_SHAFT_RADIUS = 4.0
|
||||
MOTOR_AIR_GAP = MOTOR_STATOR_TOOTH_INNER_RADIUS - MOTOR_MAGNET_OUTER_RADIUS
|
||||
|
||||
REAR_COVER_BOTTOM_Z = -37.0
|
||||
REAR_COVER_THICKNESS = 3.0
|
||||
PCB_BOTTOM_Z = -33.6
|
||||
PCB_THICKNESS = 1.6
|
||||
PCB_RADIUS = 22.2
|
||||
PCB_CENTER_BORE_RADIUS = 5.0
|
||||
PCB_STANDOFF_PCD = 33.0
|
||||
PCB_MOUNT_HOLE_RADIUS = 1.10
|
||||
REAR_COLUMN_PCD = 40.6
|
||||
REAR_COLUMN_RADIUS = 3.2
|
||||
REAR_SPIDER_BOSS_RADIUS = 2.9
|
||||
REAR_FASTENER_HOLE_RADIUS = 1.35
|
||||
REAR_BEARING_CENTER_Z = -29.0
|
||||
FRONT_MOTOR_BEARING_CENTER_Z = -2.5
|
||||
|
||||
REDUCER_HOUSING_BOTTOM_Z = -6.0
|
||||
REDUCER_HOUSING_FRONT_Z = 24.3
|
||||
REDUCER_HOUSING_INNER_RADIUS = 22.80
|
||||
RING_INSERT_OUTER_RADIUS = 22.82
|
||||
MOTOR_INTERFACE_PCD = 43.0
|
||||
OUTPUT_CAP_INTERFACE_PCD = 43.0
|
||||
M3_CLEARANCE_RADIUS = 1.60
|
||||
HOUSING_INTERFACE_LAND_INNER_RADIUS = 18.50
|
||||
|
||||
PLANET_COUNT = 3
|
||||
PRESSURE_ANGLE = 20.0
|
||||
HELIX_ANGLE = 24.0
|
||||
BACKLASH = 0.03
|
||||
ADDENDUM_FACTOR = 1.0
|
||||
CLEARANCE_FACTOR = 0.25
|
||||
RING_RIM_THICKNESS = 1.80
|
||||
RING_SUPPORT_OVERLAP = 0.30
|
||||
GEAR_HEIGHT = 5.50
|
||||
|
||||
STAGE1_CARRIER_BOTTOM_Z = 8.20
|
||||
STAGE1_CARRIER_THICKNESS = 2.50
|
||||
STAGE1_PIN_RADIUS = 1.48
|
||||
STAGE1_PIN_BOTTOM_Z = 2.25
|
||||
STAGE1_HUB_RADIUS = 4.2
|
||||
STAGE1_PAD_RADIUS = 4.0
|
||||
STAGE1_ARM_WIDTH = 3.2
|
||||
INTERSTAGE_SHAFT_RADIUS = 2.50
|
||||
|
||||
STAGE2_CARRIER_BOTTOM_Z = 20.20
|
||||
STAGE2_CARRIER_THICKNESS = 3.0
|
||||
STAGE2_PIN_RADIUS = 1.48
|
||||
STAGE2_PIN_BOTTOM_Z = 14.25
|
||||
STAGE2_HUB_RADIUS = 8.8
|
||||
STAGE2_PAD_RADIUS = 4.1
|
||||
STAGE2_ARM_WIDTH = 3.5
|
||||
OUTPUT_SHAFT_RADIUS = 7.98
|
||||
|
||||
OUTPUT_CAP_BOTTOM_Z = 24.3
|
||||
OUTPUT_CAP_CARTRIDGE_TOP_Z = 34.8
|
||||
OUTPUT_CAP_TOP_Z = 36.3
|
||||
OUTPUT_BEARING_1_CENTER_Z = 26.8
|
||||
OUTPUT_BEARING_2_CENTER_Z = 31.8
|
||||
OUTPUT_FLANGE_BOTTOM_Z = 35.3
|
||||
OUTPUT_FLANGE_TOP_Z = 38.8
|
||||
OUTPUT_FLANGE_RADIUS = 22.4
|
||||
OUTPUT_LINK_HOLE_PCD = 34.0
|
||||
OUTPUT_LINK_BOLT_COUNT = 6
|
||||
OUTPUT_LINK_BOLT_ANGLES_DEGREES = (30.0, 90.0, 150.0, 210.0, 270.0, 330.0)
|
||||
OUTPUT_LINK_TAP_RADIUS = 1.25
|
||||
OUTPUT_LINK_THREAD_DEPTH = 3.0
|
||||
OUTPUT_REGISTER_RADIUS = OUTPUT_SHAFT_RADIUS
|
||||
OUTPUT_REGISTER_HEIGHT = PACKAGE_TOP_Z - OUTPUT_FLANGE_TOP_Z
|
||||
OUTPUT_CASE_CLAMP_CENTER_Z = 20.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StageSpec:
|
||||
"""One fixed-ring planetary stage."""
|
||||
|
||||
stage_id: str
|
||||
label: str
|
||||
module: float
|
||||
sun_teeth: int
|
||||
planet_teeth: int
|
||||
bottom_z: float
|
||||
|
||||
@property
|
||||
def ring_teeth(self) -> int:
|
||||
return self.sun_teeth + 2 * self.planet_teeth
|
||||
|
||||
@property
|
||||
def top_z(self) -> float:
|
||||
return self.bottom_z + GEAR_HEIGHT
|
||||
|
||||
@property
|
||||
def mid_z(self) -> float:
|
||||
return self.bottom_z + GEAR_HEIGHT / 2.0
|
||||
|
||||
@property
|
||||
def sun_pitch_radius(self) -> float:
|
||||
return self.module * self.sun_teeth / 2.0
|
||||
|
||||
@property
|
||||
def planet_pitch_radius(self) -> float:
|
||||
return self.module * self.planet_teeth / 2.0
|
||||
|
||||
@property
|
||||
def ring_pitch_radius(self) -> float:
|
||||
return self.module * self.ring_teeth / 2.0
|
||||
|
||||
@property
|
||||
def planet_center_radius(self) -> float:
|
||||
return self.sun_pitch_radius + self.planet_pitch_radius
|
||||
|
||||
@property
|
||||
def fixed_ring_ratio(self) -> float:
|
||||
return 1.0 + self.ring_teeth / self.sun_teeth
|
||||
|
||||
@property
|
||||
def ring_outer_radius(self) -> float:
|
||||
return (
|
||||
self.ring_pitch_radius
|
||||
+ self.module * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
|
||||
+ RING_RIM_THICKNESS
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BearingSpec:
|
||||
"""Catalog-style radial ball bearing dimensions."""
|
||||
|
||||
bore_diameter: float
|
||||
outer_diameter: float
|
||||
width: float
|
||||
ball_diameter: float
|
||||
ball_count: int
|
||||
|
||||
|
||||
STAGE_1 = StageSpec(
|
||||
stage_id="stage1",
|
||||
label="Stage 1",
|
||||
module=0.80,
|
||||
sun_teeth=15,
|
||||
planet_teeth=15,
|
||||
bottom_z=2.0,
|
||||
)
|
||||
STAGE_2 = StageSpec(
|
||||
stage_id="stage2",
|
||||
label="Stage 2",
|
||||
module=0.55,
|
||||
sun_teeth=18,
|
||||
planet_teeth=27,
|
||||
bottom_z=14.0,
|
||||
)
|
||||
|
||||
REAR_MOTOR_BEARING = BearingSpec(8.0, 16.0, 5.0, 2.0, 8)
|
||||
FRONT_MOTOR_BEARING = BearingSpec(8.0, 19.0, 6.0, 2.4, 9)
|
||||
INTERSTAGE_BEARING = BearingSpec(5.0, 10.0, 3.0, 1.0, 8)
|
||||
PLANET_BEARING = BearingSpec(3.0, 6.0, 3.0, 0.7, 8)
|
||||
OUTPUT_BEARING = BearingSpec(16.0, 24.0, 5.0, 2.5, 12)
|
||||
REAR_SPIDER_BOTTOM_Z = REAR_BEARING_CENTER_Z - REAR_MOTOR_BEARING.width / 2.0
|
||||
INTERSTAGE_BEARING_CENTER_Z = (
|
||||
STAGE1_CARRIER_BOTTOM_Z + STAGE1_CARRIER_THICKNESS + STAGE_2.bottom_z
|
||||
) / 2.0
|
||||
|
||||
TOTAL_REDUCTION = STAGE_1.fixed_ring_ratio * STAGE_2.fixed_ring_ratio
|
||||
|
||||
|
||||
def validate_design_dimensions() -> None:
|
||||
"""Fail early if a packaging or minimum-ligament invariant is broken."""
|
||||
|
||||
assert MOTOR_AIR_GAP >= 0.30
|
||||
assert MOTOR_STATOR_OUTER_RADIUS == MOTOR_SHELL_INNER_RADIUS
|
||||
assert abs(PACKAGE_RADIUS - MOTOR_SHELL_INNER_RADIUS - 1.80) < 1.0e-9
|
||||
assert abs(PACKAGE_RADIUS - REDUCER_HOUSING_INNER_RADIUS - 2.20) < 1.0e-9
|
||||
assert max(STAGE_1.ring_outer_radius, STAGE_2.ring_outer_radius) < RING_INSERT_OUTER_RADIUS
|
||||
assert RING_INSERT_OUTER_RADIUS > REDUCER_HOUSING_INNER_RADIUS
|
||||
assert (
|
||||
MOTOR_INTERFACE_PCD / 2.0
|
||||
- M3_CLEARANCE_RADIUS
|
||||
- HOUSING_INTERFACE_LAND_INNER_RADIUS
|
||||
>= 1.40 - 1.0e-9
|
||||
)
|
||||
assert REAR_SPIDER_BOSS_RADIUS - REAR_FASTENER_HOLE_RADIUS >= 1.50
|
||||
assert REAR_COLUMN_RADIUS > REAR_SPIDER_BOSS_RADIUS
|
||||
assert REAR_SPIDER_BOTTOM_Z > MOTOR_SHELL_BOTTOM_Z
|
||||
assert OUTPUT_FLANGE_RADIUS <= PACKAGE_RADIUS
|
||||
assert OUTPUT_LINK_HOLE_PCD / 2.0 + OUTPUT_LINK_TAP_RADIUS < OUTPUT_FLANGE_RADIUS
|
||||
assert OUTPUT_LINK_THREAD_DEPTH < OUTPUT_FLANGE_TOP_Z - OUTPUT_FLANGE_BOTTOM_Z
|
||||
assert OUTPUT_REGISTER_HEIGHT >= 1.5
|
||||
assert OUTPUT_REGISTER_RADIUS < OUTPUT_LINK_HOLE_PCD / 2.0 - OUTPUT_LINK_TAP_RADIUS
|
||||
assert REDUCER_HOUSING_BOTTOM_Z < OUTPUT_CASE_CLAMP_CENTER_Z < REDUCER_HOUSING_FRONT_Z
|
||||
assert OUTPUT_BEARING_2_CENTER_Z - OUTPUT_BEARING_1_CENTER_Z == OUTPUT_BEARING.width
|
||||
assert abs(TOTAL_REDUCTION - 20.0) < 1.0e-9
|
||||
assert (STAGE_1.sun_teeth + STAGE_1.ring_teeth) % PLANET_COUNT == 0
|
||||
assert (STAGE_2.sun_teeth + STAGE_2.ring_teeth) % PLANET_COUNT == 0
|
||||
assert (
|
||||
INTERSTAGE_BEARING_CENTER_Z - INTERSTAGE_BEARING.width / 2.0
|
||||
> STAGE1_CARRIER_BOTTOM_Z + STAGE1_CARRIER_THICKNESS
|
||||
)
|
||||
assert (
|
||||
INTERSTAGE_BEARING_CENTER_Z + INTERSTAGE_BEARING.width / 2.0
|
||||
< STAGE_2.bottom_z
|
||||
)
|
||||
print(
|
||||
"design_dimensions: "
|
||||
f"diameter={PACKAGE_RADIUS * 2.0:.1f} structural_length="
|
||||
f"{PACKAGE_TOP_Z - PACKAGE_STRUCTURAL_BOTTOM_Z:.1f} air_gap={MOTOR_AIR_GAP:.2f} "
|
||||
f"ratio={TOTAL_REDUCTION:.1f}"
|
||||
)
|
||||
@@ -0,0 +1,270 @@
|
||||
"""Circular integrated controller PCB, power stages, and rear terminals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
try:
|
||||
from .common import (
|
||||
apply_tags,
|
||||
connector_ref,
|
||||
ground_constraint_report,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
make_axis_part_rpart,
|
||||
radial_centers,
|
||||
)
|
||||
from .dimensions import (
|
||||
PCB_BOTTOM_Z,
|
||||
PCB_CENTER_BORE_RADIUS,
|
||||
PCB_MOUNT_HOLE_RADIUS,
|
||||
PCB_RADIUS,
|
||||
PCB_STANDOFF_PCD,
|
||||
PCB_THICKNESS,
|
||||
REAR_COLUMN_PCD,
|
||||
)
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
from common import (
|
||||
apply_tags,
|
||||
connector_ref,
|
||||
ground_constraint_report,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
make_axis_part_rpart,
|
||||
radial_centers,
|
||||
)
|
||||
from dimensions import (
|
||||
PCB_BOTTOM_Z,
|
||||
PCB_CENTER_BORE_RADIUS,
|
||||
PCB_MOUNT_HOLE_RADIUS,
|
||||
PCB_RADIUS,
|
||||
PCB_STANDOFF_PCD,
|
||||
PCB_THICKNESS,
|
||||
REAR_COLUMN_PCD,
|
||||
)
|
||||
|
||||
|
||||
PHASE_TERMINAL_CENTER = (-11.0, 0.0)
|
||||
POWER_CAN_TERMINAL_CENTER = (11.0, 0.0)
|
||||
MOSFET_ANGLES = (22.5, 67.5, 112.5, 202.5, 247.5, 292.5)
|
||||
|
||||
|
||||
def make_integrated_controller_rassembly(
|
||||
*,
|
||||
pcb_material: scad.Material,
|
||||
terminal_material: scad.Material,
|
||||
) -> scad.Assembly:
|
||||
"""Build the circular ESC with six power devices and two service terminals."""
|
||||
|
||||
pcb = _make_controller_pcb_rpart(material=pcb_material)
|
||||
mosfet = _make_mosfet_package_rpart(material=terminal_material)
|
||||
phase_terminal = _make_terminal_block_rpart(
|
||||
part_id="three_phase_terminal",
|
||||
name="Three-position motor phase terminal",
|
||||
width=8.0,
|
||||
pin_count=3,
|
||||
material=terminal_material,
|
||||
)
|
||||
power_terminal = _make_terminal_block_rpart(
|
||||
part_id="power_can_terminal",
|
||||
name="Four-position DC power and CAN terminal",
|
||||
width=8.0,
|
||||
pin_count=4,
|
||||
material=terminal_material,
|
||||
)
|
||||
controller = scad.make_assembly_rassembly(
|
||||
assembly_id="integrated_circular_motor_controller",
|
||||
name="44.4 mm circular integrated BLDC controller",
|
||||
)
|
||||
controller = scad.add_component_rassembly(
|
||||
assembly=controller,
|
||||
item=pcb,
|
||||
component_id="pcb",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Circular controller PCB",
|
||||
)
|
||||
controller = scad.ground_component_rassembly(assembly=controller, component_id="pcb")
|
||||
|
||||
for index, angle in enumerate(MOSFET_ANGLES):
|
||||
radians = math.radians(angle)
|
||||
center = (13.2 * math.cos(radians), 13.2 * math.sin(radians))
|
||||
component_id = f"mosfet_{index + 1}"
|
||||
z = PCB_BOTTOM_Z + PCB_THICKNESS
|
||||
controller = scad.add_component_rassembly(
|
||||
assembly=controller,
|
||||
item=mosfet,
|
||||
component_id=component_id,
|
||||
placement=scad.make_placement_rplacement(origin=(center[0], center[1], z)),
|
||||
name=f"Power MOSFET package {index + 1}",
|
||||
)
|
||||
controller = scad.add_fixed_constraint_rassembly(
|
||||
assembly=controller,
|
||||
constraint_id=f"{component_id}_soldered",
|
||||
connector_a=connector_ref(component_id="pcb", connector_id=component_id),
|
||||
connector_b=connector_ref(component_id=component_id, connector_id="solder_axis"),
|
||||
name=f"MOSFET {index + 1} solder attachment",
|
||||
)
|
||||
|
||||
for component_id, item, center, connector_id in (
|
||||
("phase_terminal", phase_terminal, PHASE_TERMINAL_CENTER, "phase_terminal"),
|
||||
("power_can_terminal", power_terminal, POWER_CAN_TERMINAL_CENTER, "power_can_terminal"),
|
||||
):
|
||||
controller = scad.add_component_rassembly(
|
||||
assembly=controller,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=scad.make_placement_rplacement(origin=(center[0], center[1], -38.5)),
|
||||
name=item.name,
|
||||
)
|
||||
controller = scad.add_fixed_constraint_rassembly(
|
||||
assembly=controller,
|
||||
constraint_id=f"{component_id}_soldered",
|
||||
connector_a=connector_ref(component_id="pcb", connector_id=connector_id),
|
||||
connector_b=connector_ref(component_id=component_id, connector_id="solder_axis"),
|
||||
name=f"{component_id.replace('_', ' ')} solder and screw retention",
|
||||
)
|
||||
|
||||
for connector_id in ("cover_axis", "phase_access", "power_can_access"):
|
||||
controller = scad.forward_connector_rassembly(
|
||||
assembly=controller,
|
||||
connector_id=connector_id,
|
||||
source_component_id="pcb",
|
||||
source_connector_id=connector_id,
|
||||
name=connector_id.replace("_", " "),
|
||||
offset=None,
|
||||
)
|
||||
controller = scad.solve_assembly_constraints_rassembly(assembly=controller, strict=True)
|
||||
ground_constraint_report(label="controller", assembly=controller)
|
||||
print("controller_packaging: pcb_d=44.4 mosfets=6 phase_pins=3 power_can_pins=4")
|
||||
return controller
|
||||
|
||||
|
||||
def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
board = scad.make_cylinder_rsolid(
|
||||
radius=PCB_RADIUS,
|
||||
height=PCB_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, PCB_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
cutters: list[scad.Solid] = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=PCB_CENTER_BORE_RADIUS,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, PCB_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
]
|
||||
cutters.extend(
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=PCB_STANDOFF_PCD,
|
||||
hole_radius=PCB_MOUNT_HOLE_RADIUS,
|
||||
bottom_z=PCB_BOTTOM_Z - 1.0,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
angle_offset=45.0,
|
||||
)
|
||||
)
|
||||
cutters.extend(
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=REAR_COLUMN_PCD,
|
||||
hole_radius=3.45,
|
||||
bottom_z=PCB_BOTTOM_Z - 1.0,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
)
|
||||
)
|
||||
for x, count in ((PHASE_TERMINAL_CENTER[0], 3), (POWER_CAN_TERMINAL_CENTER[0], 4)):
|
||||
for pin in range(count):
|
||||
y = (pin - (count - 1) / 2.0) * 1.8
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=0.65,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
bottom_face_center=(x, y, PCB_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
board = scad.cut_rsolid(board, cutters, skip_non_intersecting=False)
|
||||
board = apply_tags(
|
||||
shape=board,
|
||||
tags=("role.circular_esc_pcb", "role.controller_mounting_holes", "group.integrated_electronics"),
|
||||
)
|
||||
connectors = [
|
||||
("cover_axis", (0.0, 0.0, PCB_BOTTOM_Z + PCB_THICKNESS / 2.0), "Rear-cover PCB plane"),
|
||||
("phase_terminal", (*PHASE_TERMINAL_CENTER, PCB_BOTTOM_Z), "Phase terminal solder datum"),
|
||||
("power_can_terminal", (*POWER_CAN_TERMINAL_CENTER, PCB_BOTTOM_Z), "Power/CAN terminal solder datum"),
|
||||
("phase_access", (*PHASE_TERMINAL_CENTER, -37.0), "Phase terminal service axis"),
|
||||
("power_can_access", (*POWER_CAN_TERMINAL_CENTER, -37.0), "Power/CAN service axis"),
|
||||
]
|
||||
for index, angle in enumerate(MOSFET_ANGLES):
|
||||
radians = math.radians(angle)
|
||||
center = (13.2 * math.cos(radians), 13.2 * math.sin(radians))
|
||||
connectors.append(
|
||||
(
|
||||
f"mosfet_{index + 1}",
|
||||
(center[0], center[1], PCB_BOTTOM_Z + PCB_THICKNESS),
|
||||
f"MOSFET {index + 1} solder datum",
|
||||
)
|
||||
)
|
||||
print("pcb_holes: center=10.0 mount=4 column_notches=4 terminal_pins=7")
|
||||
return make_axis_part_rpart(
|
||||
part_id="circular_controller_pcb",
|
||||
body=board,
|
||||
name="44.4 mm circular ESC PCB with service cutouts",
|
||||
material=material,
|
||||
connectors=connectors,
|
||||
)
|
||||
|
||||
|
||||
def _make_mosfet_package_rpart(*, material: scad.Material) -> scad.Part:
|
||||
package = scad.make_box_rsolid(
|
||||
width=4.0,
|
||||
height=3.0,
|
||||
depth=1.4,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
)
|
||||
package = apply_tags(shape=package, tags=("role.power_mosfet", "group.three_phase_bridge"))
|
||||
return make_axis_part_rpart(
|
||||
part_id="reusable_power_mosfet",
|
||||
body=package,
|
||||
name="Reusable power MOSFET package",
|
||||
material=material,
|
||||
connectors=(("solder_axis", (0.0, 0.0, 0.0), "PCB solder plane"),),
|
||||
)
|
||||
|
||||
|
||||
def _make_terminal_block_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
name: str,
|
||||
width: float,
|
||||
pin_count: int,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
body = scad.make_box_rsolid(
|
||||
width=width,
|
||||
height=6.0,
|
||||
depth=4.9,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
)
|
||||
access_cutters = []
|
||||
for pin in range(pin_count):
|
||||
y = (pin - (pin_count - 1) / 2.0) * 1.8
|
||||
access_cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=0.75,
|
||||
height=width + 2.0,
|
||||
bottom_face_center=(-width / 2.0 - 1.0, y, 2.45),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
body = scad.cut_rsolid(body, access_cutters, skip_non_intersecting=False)
|
||||
body = apply_tags(shape=body, tags=("role.rear_wiring_terminal", "role.service_access"))
|
||||
print(f"terminal_{part_id}: pins={pin_count} access_holes={pin_count} width={width:.1f}")
|
||||
return make_axis_part_rpart(
|
||||
part_id=part_id,
|
||||
body=body,
|
||||
name=name,
|
||||
material=material,
|
||||
connectors=(("solder_axis", (0.0, 0.0, 4.9), "PCB solder and screw datum"),),
|
||||
)
|
||||
@@ -0,0 +1,441 @@
|
||||
"""Herringbone ring, planet, carrier, and output parts for the 20:1 reducer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
try:
|
||||
from .common import (
|
||||
apply_tags,
|
||||
make_axis_part_rpart,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
make_z_rotation_rplacement,
|
||||
)
|
||||
from .dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CLEARANCE_FACTOR,
|
||||
GEAR_HEIGHT,
|
||||
HELIX_ANGLE,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
INTERSTAGE_SHAFT_RADIUS,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
OUTPUT_FLANGE_BOTTOM_Z,
|
||||
OUTPUT_FLANGE_RADIUS,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
OUTPUT_LINK_BOLT_ANGLES_DEGREES,
|
||||
OUTPUT_LINK_BOLT_COUNT,
|
||||
OUTPUT_LINK_HOLE_PCD,
|
||||
OUTPUT_LINK_TAP_RADIUS,
|
||||
OUTPUT_LINK_THREAD_DEPTH,
|
||||
OUTPUT_REGISTER_HEIGHT,
|
||||
OUTPUT_SHAFT_RADIUS,
|
||||
PLANET_BEARING,
|
||||
PLANET_COUNT,
|
||||
PRESSURE_ANGLE,
|
||||
RING_INSERT_OUTER_RADIUS,
|
||||
RING_RIM_THICKNESS,
|
||||
RING_SUPPORT_OVERLAP,
|
||||
STAGE1_ARM_WIDTH,
|
||||
STAGE1_CARRIER_BOTTOM_Z,
|
||||
STAGE1_CARRIER_THICKNESS,
|
||||
STAGE1_HUB_RADIUS,
|
||||
STAGE1_PAD_RADIUS,
|
||||
STAGE1_PIN_BOTTOM_Z,
|
||||
STAGE1_PIN_RADIUS,
|
||||
STAGE2_ARM_WIDTH,
|
||||
STAGE2_CARRIER_BOTTOM_Z,
|
||||
STAGE2_CARRIER_THICKNESS,
|
||||
STAGE2_HUB_RADIUS,
|
||||
STAGE2_PAD_RADIUS,
|
||||
STAGE2_PIN_BOTTOM_Z,
|
||||
STAGE2_PIN_RADIUS,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
StageSpec,
|
||||
)
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
from common import (
|
||||
apply_tags,
|
||||
make_axis_part_rpart,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
make_z_rotation_rplacement,
|
||||
)
|
||||
from dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CLEARANCE_FACTOR,
|
||||
GEAR_HEIGHT,
|
||||
HELIX_ANGLE,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
INTERSTAGE_SHAFT_RADIUS,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
OUTPUT_FLANGE_BOTTOM_Z,
|
||||
OUTPUT_FLANGE_RADIUS,
|
||||
OUTPUT_FLANGE_TOP_Z,
|
||||
OUTPUT_LINK_BOLT_ANGLES_DEGREES,
|
||||
OUTPUT_LINK_BOLT_COUNT,
|
||||
OUTPUT_LINK_HOLE_PCD,
|
||||
OUTPUT_LINK_TAP_RADIUS,
|
||||
OUTPUT_LINK_THREAD_DEPTH,
|
||||
OUTPUT_REGISTER_HEIGHT,
|
||||
OUTPUT_SHAFT_RADIUS,
|
||||
PLANET_BEARING,
|
||||
PLANET_COUNT,
|
||||
PRESSURE_ANGLE,
|
||||
RING_INSERT_OUTER_RADIUS,
|
||||
RING_RIM_THICKNESS,
|
||||
RING_SUPPORT_OVERLAP,
|
||||
STAGE1_ARM_WIDTH,
|
||||
STAGE1_CARRIER_BOTTOM_Z,
|
||||
STAGE1_CARRIER_THICKNESS,
|
||||
STAGE1_HUB_RADIUS,
|
||||
STAGE1_PAD_RADIUS,
|
||||
STAGE1_PIN_BOTTOM_Z,
|
||||
STAGE1_PIN_RADIUS,
|
||||
STAGE2_ARM_WIDTH,
|
||||
STAGE2_CARRIER_BOTTOM_Z,
|
||||
STAGE2_CARRIER_THICKNESS,
|
||||
STAGE2_HUB_RADIUS,
|
||||
STAGE2_PAD_RADIUS,
|
||||
STAGE2_PIN_BOTTOM_Z,
|
||||
STAGE2_PIN_RADIUS,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
StageSpec,
|
||||
)
|
||||
|
||||
|
||||
def make_stage_ring_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create one herringbone ring insert with a full housing support rim."""
|
||||
|
||||
ring = scad.std.gear.make_herringbone_ring_gear_rsolid(
|
||||
n_teeth=stage.ring_teeth,
|
||||
module=stage.module,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=-HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
rim_thickness=RING_RIM_THICKNESS,
|
||||
backlash=BACKLASH,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
)
|
||||
support = scad.make_cylinder_rsolid(
|
||||
radius=RING_INSERT_OUTER_RADIUS,
|
||||
height=GEAR_HEIGHT,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
support_bore = scad.make_cylinder_rsolid(
|
||||
radius=stage.ring_outer_radius - RING_SUPPORT_OVERLAP,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
support = scad.cut_rsolid(support, support_bore, skip_non_intersecting=False)
|
||||
ring = scad.union_rsolid(ring, support, glue=False)
|
||||
ring = apply_tags(
|
||||
shape=ring,
|
||||
tags=(f"role.{stage.stage_id}.fixed_ring_gear", "role.ring_gear_press_fit", "group.two_stage_reducer"),
|
||||
)
|
||||
print(
|
||||
f"{stage.stage_id}_ring: teeth={stage.ring_teeth} pitch_r={stage.ring_pitch_radius:.3f} "
|
||||
f"toothed_outer_r={stage.ring_outer_radius:.3f} insert_d={RING_INSERT_OUTER_RADIUS * 2.0:.2f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_fixed_ring",
|
||||
body=ring,
|
||||
name=f"{stage.label} replaceable fixed herringbone ring insert",
|
||||
material=material,
|
||||
connectors=(("axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Fixed ring axis"),),
|
||||
)
|
||||
|
||||
|
||||
def make_stage_planet_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create one reusable herringbone planet with a standard-bearing seat."""
|
||||
|
||||
planet = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=stage.planet_teeth,
|
||||
module=stage.module,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=-HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
bearing_seat_radius = PLANET_BEARING.outer_diameter / 2.0 + 0.05
|
||||
bearing_seat = scad.make_cylinder_rsolid(
|
||||
radius=bearing_seat_radius,
|
||||
height=GEAR_HEIGHT + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
planet = scad.cut_rsolid(planet, bearing_seat, skip_non_intersecting=False)
|
||||
planet = apply_tags(
|
||||
shape=planet,
|
||||
tags=(f"role.{stage.stage_id}.planet_gear", "role.planet_bearing_seat", "group.two_stage_reducer"),
|
||||
)
|
||||
root_radius = stage.planet_pitch_radius - stage.module * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
|
||||
print(
|
||||
f"{stage.stage_id}_planet: teeth={stage.planet_teeth} pitch_r={stage.planet_pitch_radius:.3f} "
|
||||
f"bearing_seat_r={bearing_seat_radius:.3f} root_ligament={root_radius - bearing_seat_radius:.3f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id=f"{stage.stage_id}_reusable_planet",
|
||||
body=planet,
|
||||
name=f"{stage.label} reusable bearing-supported planet",
|
||||
material=material,
|
||||
connectors=(
|
||||
("axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Planet spin axis"),
|
||||
("bearing_axis", (0.0, 0.0, GEAR_HEIGHT / 2.0), "Planet bearing outer-ring axis"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the first carrier and integral second-stage sun/shaft."""
|
||||
|
||||
carrier = _make_carrier_body_rsolid(
|
||||
stage=STAGE_1,
|
||||
plate_bottom_z=STAGE1_CARRIER_BOTTOM_Z,
|
||||
plate_thickness=STAGE1_CARRIER_THICKNESS,
|
||||
pin_bottom_z=STAGE1_PIN_BOTTOM_Z,
|
||||
pin_radius=STAGE1_PIN_RADIUS,
|
||||
hub_radius=STAGE1_HUB_RADIUS,
|
||||
arm_width=STAGE1_ARM_WIDTH,
|
||||
pad_radius=STAGE1_PAD_RADIUS,
|
||||
)
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=INTERSTAGE_SHAFT_RADIUS,
|
||||
height=STAGE_2.top_z - STAGE1_CARRIER_BOTTOM_Z + 0.1,
|
||||
bottom_face_center=(0.0, 0.0, STAGE1_CARRIER_BOTTOM_Z - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
stage2_sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=STAGE_2.sun_teeth,
|
||||
module=STAGE_2.module,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
stage2_sun = scad.translate_shape(shape=stage2_sun, vector=(0.0, 0.0, STAGE_2.bottom_z))
|
||||
carrier = scad.union_rsolid(carrier, shaft, stage2_sun, glue=False)
|
||||
carrier = apply_tags(
|
||||
shape=carrier,
|
||||
tags=("role.stage1.planet_carrier", "role.stage2.sun_gear", "role.integral_interstage_drive", "group.two_stage_reducer"),
|
||||
)
|
||||
connectors = [
|
||||
("carrier_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Stage 1 carrier bearing axis"),
|
||||
(
|
||||
"interstage_bearing_axis",
|
||||
(0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z),
|
||||
"Interstage bearing inner-ring seat",
|
||||
),
|
||||
("stage2_sun_axis", (0.0, 0.0, STAGE_2.mid_z), "Integral stage 2 sun axis"),
|
||||
]
|
||||
for index in range(PLANET_COUNT):
|
||||
center = planet_center_xy(stage=STAGE_1, index=index)
|
||||
connectors.extend(
|
||||
(
|
||||
(f"planet_{index + 1}_axis", (*center, STAGE_1.mid_z), f"Stage 1 planet {index + 1} axis"),
|
||||
(f"planet_{index + 1}_bearing_axis", (*center, STAGE_1.mid_z), f"Stage 1 planet {index + 1} bearing pin"),
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"stage1_carrier_sun: pins={PLANET_COUNT} shaft_d={INTERSTAGE_SHAFT_RADIUS * 2.0:.2f} "
|
||||
f"stage2_sun_teeth={STAGE_2.sun_teeth}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="stage1_carrier_integral_stage2_sun",
|
||||
body=carrier,
|
||||
name="Stage 1 carrier with integral stage-2 sun shaft",
|
||||
material=material,
|
||||
connectors=connectors,
|
||||
)
|
||||
|
||||
|
||||
def make_output_carrier_flange_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create the second carrier, 16 mm bearing land, and output flange."""
|
||||
|
||||
carrier = _make_carrier_body_rsolid(
|
||||
stage=stage,
|
||||
plate_bottom_z=STAGE2_CARRIER_BOTTOM_Z,
|
||||
plate_thickness=STAGE2_CARRIER_THICKNESS,
|
||||
pin_bottom_z=STAGE2_PIN_BOTTOM_Z,
|
||||
pin_radius=STAGE2_PIN_RADIUS,
|
||||
hub_radius=STAGE2_HUB_RADIUS,
|
||||
arm_width=STAGE2_ARM_WIDTH,
|
||||
pad_radius=STAGE2_PAD_RADIUS,
|
||||
)
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_SHAFT_RADIUS,
|
||||
height=(
|
||||
OUTPUT_FLANGE_TOP_Z
|
||||
+ OUTPUT_REGISTER_HEIGHT
|
||||
- STAGE2_CARRIER_BOTTOM_Z
|
||||
+ 0.05
|
||||
),
|
||||
bottom_face_center=(0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
flange = scad.make_cylinder_rsolid(
|
||||
radius=OUTPUT_FLANGE_RADIUS,
|
||||
height=OUTPUT_FLANGE_TOP_Z - OUTPUT_FLANGE_BOTTOM_Z,
|
||||
bottom_face_center=(0.0, 0.0, OUTPUT_FLANGE_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
output = scad.union_rsolid(carrier, shaft, flange, glue=False)
|
||||
output = scad.cut_rsolid(
|
||||
output,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=OUTPUT_LINK_BOLT_COUNT,
|
||||
pcd=OUTPUT_LINK_HOLE_PCD,
|
||||
hole_radius=OUTPUT_LINK_TAP_RADIUS,
|
||||
bottom_z=OUTPUT_FLANGE_TOP_Z - OUTPUT_LINK_THREAD_DEPTH,
|
||||
height=OUTPUT_LINK_THREAD_DEPTH + 1.0,
|
||||
angle_offset=OUTPUT_LINK_BOLT_ANGLES_DEGREES[0],
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
output = apply_tags(
|
||||
shape=output,
|
||||
tags=("role.stage2.output_carrier", "role.output_bearing_land", "role.output_link_flange", "group.two_stage_reducer"),
|
||||
)
|
||||
connectors = [
|
||||
("carrier_axis", (0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z + STAGE2_CARRIER_THICKNESS / 2.0), "Stage 2 output carrier axis"),
|
||||
("bearing_1_axis", (0.0, 0.0, OUTPUT_BEARING_1_CENTER_Z), "Rear output bearing inner-ring seat"),
|
||||
("bearing_2_axis", (0.0, 0.0, OUTPUT_BEARING_2_CENTER_Z), "Front output bearing inner-ring seat"),
|
||||
("output_link_axis", (0.0, 0.0, OUTPUT_FLANGE_TOP_Z), "Six-hole driven-link flange"),
|
||||
]
|
||||
for index in range(PLANET_COUNT):
|
||||
center = planet_center_xy(stage=stage, index=index)
|
||||
connectors.extend(
|
||||
(
|
||||
(f"planet_{index + 1}_axis", (*center, stage.mid_z), f"Stage 2 planet {index + 1} axis"),
|
||||
(f"planet_{index + 1}_bearing_axis", (*center, stage.mid_z), f"Stage 2 planet {index + 1} bearing pin"),
|
||||
)
|
||||
)
|
||||
radial_ligament = OUTPUT_FLANGE_RADIUS - (OUTPUT_LINK_HOLE_PCD / 2.0 + OUTPUT_LINK_TAP_RADIUS)
|
||||
print(
|
||||
f"output_carrier_flange: shaft_d={OUTPUT_SHAFT_RADIUS * 2.0:.2f} "
|
||||
f"pilot_h={OUTPUT_REGISTER_HEIGHT:.1f} tapped_holes={OUTPUT_LINK_BOLT_COUNT} "
|
||||
f"pcd={OUTPUT_LINK_HOLE_PCD:.1f} thread_depth={OUTPUT_LINK_THREAD_DEPTH:.1f} "
|
||||
f"radial_ligament={radial_ligament:.2f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="stage2_output_carrier_flange",
|
||||
body=output,
|
||||
name="Stage 2 carrier with paired-bearing shaft and output flange",
|
||||
material=material,
|
||||
connectors=connectors,
|
||||
)
|
||||
|
||||
|
||||
def make_planet_rplacement(*, stage: StageSpec, index: int) -> scad.Placement:
|
||||
"""Place and visually phase one planet at its pitch center."""
|
||||
|
||||
center = planet_center_xy(stage=stage, index=index)
|
||||
carrier_angle = 360.0 * index / PLANET_COUNT
|
||||
spin = carrier_angle + 180.0 - 180.0 / stage.planet_teeth
|
||||
print(
|
||||
f"{stage.stage_id}_planet_{index + 1}_placement: center="
|
||||
f"({center[0]:.3f},{center[1]:.3f},{stage.bottom_z:.3f}) spin={spin:.2f}"
|
||||
)
|
||||
return make_z_rotation_rplacement(
|
||||
origin=(center[0], center[1], stage.bottom_z),
|
||||
angle_degrees=spin,
|
||||
)
|
||||
|
||||
|
||||
def planet_center_xy(*, stage: StageSpec, index: int) -> tuple[float, float]:
|
||||
"""Return one equally spaced planet pitch center."""
|
||||
|
||||
angle = math.radians(360.0 * index / PLANET_COUNT)
|
||||
return (
|
||||
stage.planet_center_radius * math.cos(angle),
|
||||
stage.planet_center_radius * math.sin(angle),
|
||||
)
|
||||
|
||||
|
||||
def _make_carrier_body_rsolid(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
plate_bottom_z: float,
|
||||
plate_thickness: float,
|
||||
pin_bottom_z: float,
|
||||
pin_radius: float,
|
||||
hub_radius: float,
|
||||
arm_width: float,
|
||||
pad_radius: float,
|
||||
) -> scad.Solid:
|
||||
hub = scad.make_cylinder_rsolid(
|
||||
radius=hub_radius,
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(0.0, 0.0, plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
solids = [hub]
|
||||
pin_height = plate_bottom_z + plate_thickness - pin_bottom_z
|
||||
arm_inner_radius = hub_radius - 1.25
|
||||
arm_outer_radius = stage.planet_center_radius + pad_radius - 0.25
|
||||
arm_length = arm_outer_radius - arm_inner_radius
|
||||
arm_center_radius = (arm_inner_radius + arm_outer_radius) / 2.0
|
||||
for index in range(PLANET_COUNT):
|
||||
angle = 360.0 * index / PLANET_COUNT
|
||||
center = planet_center_xy(stage=stage, index=index)
|
||||
arm = scad.make_box_rsolid(
|
||||
width=arm_length,
|
||||
height=arm_width,
|
||||
depth=plate_thickness,
|
||||
bottom_face_center=(arm_center_radius, 0.0, plate_bottom_z),
|
||||
)
|
||||
solids.append(
|
||||
scad.rotate_shape(
|
||||
shape=arm,
|
||||
angle=angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=pad_radius,
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(center[0], center[1], plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=pin_radius,
|
||||
height=pin_height,
|
||||
bottom_face_center=(center[0], center[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
carrier = scad.union_rsolid(solids, glue=False)
|
||||
print(
|
||||
f"{stage.stage_id}_carrier_body: arm_length={arm_length:.3f} "
|
||||
f"hub_embed={hub_radius - arm_inner_radius:.3f} pin_height={pin_height:.3f}"
|
||||
)
|
||||
return carrier
|
||||
@@ -0,0 +1,474 @@
|
||||
"""Serviceable motor shell, reducer case, bearing caps, and electronics cover."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
try:
|
||||
from .common import (
|
||||
apply_tags,
|
||||
make_annulus_rsolid,
|
||||
make_axis_part_rpart,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
radial_centers,
|
||||
)
|
||||
from .dimensions import (
|
||||
FRONT_MOTOR_BEARING,
|
||||
FRONT_MOTOR_BEARING_CENTER_Z,
|
||||
HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
INTERSTAGE_BEARING,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
MOTOR_INTERFACE_PCD,
|
||||
MOTOR_SHELL_BOTTOM_Z,
|
||||
MOTOR_SHELL_INNER_RADIUS,
|
||||
MOTOR_SHELL_TOP_Z,
|
||||
MOTOR_STATOR_BOTTOM_Z,
|
||||
MOTOR_STATOR_TOP_Z,
|
||||
M3_CLEARANCE_RADIUS,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
OUTPUT_CAP_BOTTOM_Z,
|
||||
OUTPUT_CAP_CARTRIDGE_TOP_Z,
|
||||
OUTPUT_CAP_INTERFACE_PCD,
|
||||
OUTPUT_CAP_TOP_Z,
|
||||
OUTPUT_CASE_CLAMP_CENTER_Z,
|
||||
OUTPUT_FLANGE_RADIUS,
|
||||
PACKAGE_RADIUS,
|
||||
PCB_BOTTOM_Z,
|
||||
PCB_STANDOFF_PCD,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
REAR_COLUMN_PCD,
|
||||
REAR_COLUMN_RADIUS,
|
||||
REAR_COVER_BOTTOM_Z,
|
||||
REAR_COVER_THICKNESS,
|
||||
REAR_FASTENER_HOLE_RADIUS,
|
||||
REAR_SPIDER_BOSS_RADIUS,
|
||||
REAR_SPIDER_BOTTOM_Z,
|
||||
REDUCER_HOUSING_BOTTOM_Z,
|
||||
REDUCER_HOUSING_FRONT_Z,
|
||||
REDUCER_HOUSING_INNER_RADIUS,
|
||||
STAGE1_CARRIER_BOTTOM_Z,
|
||||
STAGE2_CARRIER_BOTTOM_Z,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
)
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
from common import (
|
||||
apply_tags,
|
||||
make_annulus_rsolid,
|
||||
make_axial_hole_cutters_rsolids,
|
||||
make_axis_part_rpart,
|
||||
radial_centers,
|
||||
)
|
||||
from dimensions import (
|
||||
FRONT_MOTOR_BEARING,
|
||||
FRONT_MOTOR_BEARING_CENTER_Z,
|
||||
HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
INTERSTAGE_BEARING,
|
||||
INTERSTAGE_BEARING_CENTER_Z,
|
||||
M3_CLEARANCE_RADIUS,
|
||||
MOTOR_INTERFACE_PCD,
|
||||
MOTOR_SHELL_BOTTOM_Z,
|
||||
MOTOR_SHELL_INNER_RADIUS,
|
||||
MOTOR_SHELL_TOP_Z,
|
||||
MOTOR_STATOR_BOTTOM_Z,
|
||||
MOTOR_STATOR_TOP_Z,
|
||||
OUTPUT_BEARING_1_CENTER_Z,
|
||||
OUTPUT_BEARING_2_CENTER_Z,
|
||||
OUTPUT_CAP_BOTTOM_Z,
|
||||
OUTPUT_CAP_CARTRIDGE_TOP_Z,
|
||||
OUTPUT_CAP_INTERFACE_PCD,
|
||||
OUTPUT_CAP_TOP_Z,
|
||||
OUTPUT_CASE_CLAMP_CENTER_Z,
|
||||
OUTPUT_FLANGE_RADIUS,
|
||||
PACKAGE_RADIUS,
|
||||
PCB_BOTTOM_Z,
|
||||
PCB_STANDOFF_PCD,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
REAR_COLUMN_PCD,
|
||||
REAR_COLUMN_RADIUS,
|
||||
REAR_COVER_BOTTOM_Z,
|
||||
REAR_COVER_THICKNESS,
|
||||
REAR_FASTENER_HOLE_RADIUS,
|
||||
REAR_SPIDER_BOSS_RADIUS,
|
||||
REAR_SPIDER_BOTTOM_Z,
|
||||
REDUCER_HOUSING_BOTTOM_Z,
|
||||
REDUCER_HOUSING_FRONT_Z,
|
||||
REDUCER_HOUSING_INNER_RADIUS,
|
||||
STAGE1_CARRIER_BOTTOM_Z,
|
||||
STAGE2_CARRIER_BOTTOM_Z,
|
||||
STAGE_1,
|
||||
STAGE_2,
|
||||
)
|
||||
|
||||
|
||||
def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the stator sleeve, front attachment land, and rear columns."""
|
||||
|
||||
sleeve = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=MOTOR_SHELL_INNER_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_BOTTOM_Z,
|
||||
height=MOTOR_SHELL_TOP_Z - MOTOR_SHELL_BOTTOM_Z,
|
||||
tags=("role.motor_shell", "role.stator_thermal_path"),
|
||||
)
|
||||
front_land = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_TOP_Z - 1.4,
|
||||
height=1.4,
|
||||
tags=("role.motor_reducer_mount",),
|
||||
)
|
||||
columns = []
|
||||
for _index, _angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
|
||||
columns.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=REAR_COLUMN_RADIUS,
|
||||
height=REAR_SPIDER_BOTTOM_Z - MOTOR_SHELL_BOTTOM_Z,
|
||||
bottom_face_center=(center[0], center[1], MOTOR_SHELL_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
shell = scad.union_rsolid(sleeve, front_land, columns, glue=False)
|
||||
shell = scad.cut_rsolid(
|
||||
shell,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=6,
|
||||
pcd=MOTOR_INTERFACE_PCD,
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_TOP_Z - 2.8,
|
||||
height=3.6,
|
||||
angle_offset=30.0,
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=REAR_COLUMN_PCD,
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_BOTTOM_Z - 1.0,
|
||||
height=10.4,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
shell = apply_tags(
|
||||
shape=shell,
|
||||
tags=("role.fixed_motor_housing", "group.integrated_bldc_actuator"),
|
||||
)
|
||||
print(
|
||||
f"motor_shell_interface: stator_d={MOTOR_SHELL_INNER_RADIUS * 2.0:.2f} "
|
||||
f"front_holes=6 rear_columns=4 wall={PACKAGE_RADIUS - MOTOR_SHELL_INNER_RADIUS:.2f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="motor_shell",
|
||||
body=shell,
|
||||
name="50 mm BLDC motor shell with rear structural columns",
|
||||
material=material,
|
||||
connectors=(
|
||||
("reducer_mount_axis", (0.0, 0.0, MOTOR_SHELL_TOP_Z), "Six-screw reducer mount"),
|
||||
(
|
||||
"stator_axis",
|
||||
(0.0, 0.0, (MOTOR_STATOR_BOTTOM_Z + MOTOR_STATOR_TOP_Z) / 2.0),
|
||||
"Stator thermal press-fit axis",
|
||||
),
|
||||
("rear_spider_axis", (0.0, 0.0, REAR_SPIDER_BOTTOM_Z), "Rear bearing spider mount"),
|
||||
("rear_cover_axis", (0.0, 0.0, MOTOR_SHELL_BOTTOM_Z), "Rear electronics cover mount"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the reducer sleeve and front motor-bearing bulkhead."""
|
||||
|
||||
sleeve = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=REDUCER_HOUSING_INNER_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_BOTTOM_Z,
|
||||
height=REDUCER_HOUSING_FRONT_Z - REDUCER_HOUSING_BOTTOM_Z,
|
||||
tags=("role.reducer_housing_sleeve",),
|
||||
)
|
||||
bulkhead = make_annulus_rsolid(
|
||||
outer_radius=23.10,
|
||||
inner_radius=FRONT_MOTOR_BEARING.outer_diameter / 2.0 + 0.05,
|
||||
bottom_z=REDUCER_HOUSING_BOTTOM_Z,
|
||||
height=STAGE_1.bottom_z - REDUCER_HOUSING_BOTTOM_Z,
|
||||
tags=("role.motor_front_bearing_bulkhead",),
|
||||
)
|
||||
interstage_divider = make_annulus_rsolid(
|
||||
outer_radius=23.10,
|
||||
inner_radius=INTERSTAGE_BEARING.outer_diameter / 2.0 + 0.05,
|
||||
bottom_z=INTERSTAGE_BEARING_CENTER_Z - INTERSTAGE_BEARING.width / 2.0,
|
||||
height=INTERSTAGE_BEARING.width,
|
||||
tags=("role.interstage_bearing_divider",),
|
||||
)
|
||||
output_mount_land = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
|
||||
height=2.2,
|
||||
tags=("role.output_cap_mount_land",),
|
||||
)
|
||||
housing = scad.union_rsolid(
|
||||
sleeve,
|
||||
bulkhead,
|
||||
interstage_divider,
|
||||
output_mount_land,
|
||||
glue=False,
|
||||
)
|
||||
housing = scad.cut_rsolid(
|
||||
housing,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=6,
|
||||
pcd=MOTOR_INTERFACE_PCD,
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_BOTTOM_Z - 1.0,
|
||||
height=STAGE_1.bottom_z - REDUCER_HOUSING_BOTTOM_Z + 2.0,
|
||||
angle_offset=30.0,
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=6,
|
||||
pcd=OUTPUT_CAP_INTERFACE_PCD,
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
|
||||
height=3.2,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
housing = apply_tags(
|
||||
shape=housing,
|
||||
tags=("role.fixed_reducer_housing", "role.ring_gear_press_fit", "group.integrated_bldc_actuator"),
|
||||
)
|
||||
print(
|
||||
f"reducer_housing: bore_d={REDUCER_HOUSING_INNER_RADIUS * 2.0:.2f} "
|
||||
f"wall={PACKAGE_RADIUS - REDUCER_HOUSING_INNER_RADIUS:.2f} bulkhead=8.00 "
|
||||
f"interstage_bearing_z={INTERSTAGE_BEARING_CENTER_Z:.2f} "
|
||||
f"m3_inner_ligament={MOTOR_INTERFACE_PCD / 2.0 - M3_CLEARANCE_RADIUS - HOUSING_INTERFACE_LAND_INNER_RADIUS:.2f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="reducer_housing",
|
||||
body=housing,
|
||||
name="50 mm reducer housing with motor bearing bulkhead",
|
||||
material=material,
|
||||
connectors=(
|
||||
("motor_mount_axis", (0.0, 0.0, MOTOR_SHELL_TOP_Z), "Motor shell six-screw interface"),
|
||||
("front_motor_bearing_axis", (0.0, 0.0, FRONT_MOTOR_BEARING_CENTER_Z), "Front motor bearing seat"),
|
||||
("stage1_ring_axis", (0.0, 0.0, STAGE_1.mid_z), "Stage 1 fixed ring seat"),
|
||||
("stage1_carrier_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Stage 1 carrier axis"),
|
||||
("interstage_bearing_axis", (0.0, 0.0, INTERSTAGE_BEARING_CENTER_Z), "Interstage bearing outer seat"),
|
||||
("stage2_ring_axis", (0.0, 0.0, STAGE_2.mid_z), "Stage 2 fixed ring seat"),
|
||||
("stage2_carrier_axis", (0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z + 1.50), "Output carrier axis"),
|
||||
(
|
||||
"case_clamp_axis",
|
||||
(0.0, 0.0, OUTPUT_CASE_CLAMP_CENTER_Z),
|
||||
"External split-clamp datum on reducer sleeve",
|
||||
),
|
||||
("output_cap_axis", (0.0, 0.0, REDUCER_HOUSING_FRONT_Z), "Output bearing cap interface"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create a four-arm removable rear motor-bearing support."""
|
||||
|
||||
bottom_z = REAR_SPIDER_BOTTOM_Z
|
||||
hub = make_annulus_rsolid(
|
||||
outer_radius=10.5,
|
||||
inner_radius=8.05,
|
||||
bottom_z=bottom_z,
|
||||
height=5.0,
|
||||
tags=("role.rear_motor_bearing_seat",),
|
||||
)
|
||||
solids = [hub]
|
||||
for _index, angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
|
||||
arm = scad.make_box_rsolid(
|
||||
width=12.0,
|
||||
height=3.0,
|
||||
depth=5.0,
|
||||
bottom_face_center=(14.5, 0.0, bottom_z),
|
||||
)
|
||||
solids.append(
|
||||
scad.rotate_shape(
|
||||
shape=arm,
|
||||
angle=angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=REAR_SPIDER_BOSS_RADIUS,
|
||||
height=5.0,
|
||||
bottom_face_center=(center[0], center[1], bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
spider = scad.union_rsolid(solids, glue=False)
|
||||
spider = scad.cut_rsolid(
|
||||
spider,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=REAR_COLUMN_PCD,
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=bottom_z - 1.0,
|
||||
height=7.0,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
spider = apply_tags(
|
||||
shape=spider,
|
||||
tags=("role.removable_rear_bearing_spider", "group.integrated_bldc_actuator"),
|
||||
)
|
||||
print("rear_bearing_spider: arms=4 bearing_seat_d=16.10 fasteners=4")
|
||||
return make_axis_part_rpart(
|
||||
part_id="rear_bearing_spider",
|
||||
body=spider,
|
||||
name="Four-arm removable rear motor-bearing spider",
|
||||
material=material,
|
||||
connectors=(
|
||||
("shell_axis", (0.0, 0.0, REAR_SPIDER_BOTTOM_Z), "Motor shell column interface"),
|
||||
("bearing_axis", (0.0, 0.0, REAR_BEARING_CENTER_Z), "Rear motor bearing outer seat"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the rear cover with PCB standoffs and terminal apertures."""
|
||||
|
||||
cover = scad.make_cylinder_rsolid(
|
||||
radius=PACKAGE_RADIUS,
|
||||
height=REAR_COVER_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, REAR_COVER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
standoffs = []
|
||||
for _index, _angle, center in radial_centers(count=4, radius=PCB_STANDOFF_PCD / 2.0, angle_offset=45.0):
|
||||
standoffs.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=2.4,
|
||||
height=PCB_BOTTOM_Z - REAR_COVER_BOTTOM_Z - REAR_COVER_THICKNESS + 0.1,
|
||||
bottom_face_center=(center[0], center[1], REAR_COVER_BOTTOM_Z + REAR_COVER_THICKNESS - 0.1),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
cover = scad.union_rsolid(cover, standoffs, glue=False)
|
||||
phase_aperture = scad.make_box_rsolid(
|
||||
width=9.2,
|
||||
height=7.2,
|
||||
depth=REAR_COVER_THICKNESS + 2.0,
|
||||
bottom_face_center=(-11.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
|
||||
)
|
||||
power_aperture = scad.make_box_rsolid(
|
||||
width=9.2,
|
||||
height=7.2,
|
||||
depth=REAR_COVER_THICKNESS + 2.0,
|
||||
bottom_face_center=(11.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
|
||||
)
|
||||
center_service = scad.make_cylinder_rsolid(
|
||||
radius=3.2,
|
||||
height=REAR_COVER_THICKNESS + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
cover = scad.cut_rsolid(
|
||||
cover,
|
||||
phase_aperture,
|
||||
power_aperture,
|
||||
center_service,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=REAR_COLUMN_PCD,
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
|
||||
height=REAR_COVER_THICKNESS + 2.0,
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
pcd=PCB_STANDOFF_PCD,
|
||||
hole_radius=1.1,
|
||||
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
|
||||
height=PCB_BOTTOM_Z - REAR_COVER_BOTTOM_Z + 2.0,
|
||||
angle_offset=45.0,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
cover = apply_tags(
|
||||
shape=cover,
|
||||
tags=("role.rear_electronics_cover", "role.terminal_access", "group.integrated_bldc_actuator"),
|
||||
)
|
||||
print("rear_cover_access: phase_opening=9.2x7.2 power_can_opening=9.2x7.2 pcb_holes=4")
|
||||
return make_axis_part_rpart(
|
||||
part_id="rear_electronics_cover",
|
||||
body=cover,
|
||||
name="Rear electronics cover with terminal access",
|
||||
material=material,
|
||||
connectors=(
|
||||
("shell_axis", (0.0, 0.0, MOTOR_SHELL_BOTTOM_Z), "Four-screw motor shell interface"),
|
||||
("pcb_axis", (0.0, 0.0, PCB_BOTTOM_Z + 0.8), "Controller PCB mounting plane"),
|
||||
("phase_access", (-11.0, 0.0, REAR_COVER_BOTTOM_Z), "Three-phase terminal access"),
|
||||
("power_can_access", (11.0, 0.0, REAR_COVER_BOTTOM_Z), "Power and CAN terminal access"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the removable paired-bearing cartridge and front cap."""
|
||||
|
||||
bearing_clearance_radius = 12.05
|
||||
rear_flange = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=bearing_clearance_radius,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z,
|
||||
height=3.0,
|
||||
tags=("role.output_cap_mount_flange",),
|
||||
)
|
||||
cartridge = make_annulus_rsolid(
|
||||
outer_radius=15.0,
|
||||
inner_radius=bearing_clearance_radius,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z,
|
||||
height=OUTPUT_CAP_CARTRIDGE_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 0.1,
|
||||
tags=("role.paired_output_bearing_seat",),
|
||||
)
|
||||
bearing_retainer = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=8.10,
|
||||
bottom_z=OUTPUT_CAP_CARTRIDGE_TOP_Z - 0.1,
|
||||
height=0.5,
|
||||
tags=("role.output_axial_retainer",),
|
||||
)
|
||||
outer_lip = make_annulus_rsolid(
|
||||
outer_radius=PACKAGE_RADIUS,
|
||||
inner_radius=OUTPUT_FLANGE_RADIUS + 0.30,
|
||||
bottom_z=OUTPUT_CAP_CARTRIDGE_TOP_Z - 0.1,
|
||||
height=OUTPUT_CAP_TOP_Z - OUTPUT_CAP_CARTRIDGE_TOP_Z + 0.1,
|
||||
tags=("role.output_labyrinth_lip",),
|
||||
)
|
||||
cap = scad.union_rsolid(rear_flange, cartridge, bearing_retainer, outer_lip, glue=False)
|
||||
cap = scad.cut_rsolid(
|
||||
cap,
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=6,
|
||||
pcd=OUTPUT_CAP_INTERFACE_PCD,
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z - 1.0,
|
||||
height=OUTPUT_CAP_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 2.0,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
cap = apply_tags(
|
||||
shape=cap,
|
||||
tags=("role.removable_output_bearing_cap", "group.integrated_bldc_actuator"),
|
||||
)
|
||||
print(
|
||||
f"output_bearing_cap: bearing_seat_d={bearing_clearance_radius * 2.0:.2f} "
|
||||
f"bearing_span={OUTPUT_BEARING_2_CENTER_Z - OUTPUT_BEARING_1_CENTER_Z:.1f} fasteners=6"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="output_bearing_cap",
|
||||
body=cap,
|
||||
name="Paired output-bearing cartridge and removable cap",
|
||||
material=material,
|
||||
connectors=(
|
||||
("housing_axis", (0.0, 0.0, OUTPUT_CAP_BOTTOM_Z), "Six-screw housing interface"),
|
||||
("bearing_1_axis", (0.0, 0.0, OUTPUT_BEARING_1_CENTER_Z), "Rear output bearing seat"),
|
||||
("bearing_2_axis", (0.0, 0.0, OUTPUT_BEARING_2_CENTER_Z), "Front output bearing seat"),
|
||||
("case_mount_axis", (0.0, 0.0, OUTPUT_CAP_TOP_Z), "Fixed actuator case datum"),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Build, validate, replay, and export the integrated BLDC joint actuator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from assembly import make_integrated_bldc_joint_actuator_rassembly
|
||||
from common import ground_compound
|
||||
from dimensions import (
|
||||
MOTOR_AIR_GAP,
|
||||
MOTOR_POLE_COUNT,
|
||||
MOTOR_SLOT_COUNT,
|
||||
PACKAGE_RADIUS,
|
||||
PACKAGE_STRUCTURAL_BOTTOM_Z,
|
||||
PACKAGE_TOP_Z,
|
||||
TOTAL_REDUCTION,
|
||||
validate_design_dimensions,
|
||||
)
|
||||
from materials import make_actuator_materials_rdict
|
||||
|
||||
|
||||
sys.setrecursionlimit(30000)
|
||||
|
||||
OUT_DIR = Path("examples/out/integrated_bldc_joint_actuator")
|
||||
|
||||
|
||||
def build_integrated_bldc_joint_actuator():
|
||||
"""Build the replayable actuator and return product and interchange outputs."""
|
||||
|
||||
validate_design_dimensions()
|
||||
materials = make_actuator_materials_rdict()
|
||||
with scad.GraphSession(graph_id="integrated_50mm_bldc_joint_actuator") as session:
|
||||
assembly = make_integrated_bldc_joint_actuator_rassembly(materials=materials)
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
ground_compound(label="integrated_actuator_preview", compound=preview)
|
||||
session_json = scad.export_session_json(session=session, indent=2)
|
||||
model_json = scad.export_model_json(session=session, indent=2)
|
||||
return assembly, preview, model_json, session_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate canonical model JSON, STEP, and optional FreeCAD output."""
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
model_path = OUT_DIR / "integrated_bldc_joint_actuator.model.json"
|
||||
session_path = OUT_DIR / "integrated_bldc_joint_actuator.session.json"
|
||||
step_path = OUT_DIR / "integrated_bldc_joint_actuator.step"
|
||||
fcstd_path = OUT_DIR / "integrated_bldc_joint_actuator.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json, session_json = build_integrated_bldc_joint_actuator()
|
||||
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, strict=True)
|
||||
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="Integrated50mmBLDCJointActuator",
|
||||
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"envelope_diameter={PACKAGE_RADIUS * 2.0:.1f}")
|
||||
print(f"structural_length={PACKAGE_TOP_Z - PACKAGE_STRUCTURAL_BOTTOM_Z:.1f}")
|
||||
print(f"motor_topology={MOTOR_SLOT_COUNT}_slot_{MOTOR_POLE_COUNT}_pole")
|
||||
print(f"motor_air_gap={MOTOR_AIR_GAP:.2f}")
|
||||
print(f"total_reduction={TOTAL_REDUCTION:.1f}")
|
||||
print(f"assembly={assembly.assembly_id}")
|
||||
print(f"components={len(assembly.component_ids())}")
|
||||
print(f"constraints={len(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()
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Material records for structural, magnetic, and electronic parts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def make_actuator_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create the materials used by Case 20."""
|
||||
|
||||
materials = {
|
||||
"housing": scad.make_material_rmaterial(
|
||||
material_id="aluminum_6061_t6",
|
||||
name="Hard-anodized 6061-T6 aluminum",
|
||||
density=2.70e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.12, 0.15, 0.18),
|
||||
),
|
||||
"carrier": scad.make_material_rmaterial(
|
||||
material_id="aluminum_7075_t6",
|
||||
name="7075-T6 aluminum",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.72, 0.74, 0.78),
|
||||
),
|
||||
"gear": scad.make_material_rmaterial(
|
||||
material_id="case_hardened_gear_steel",
|
||||
name="Case-hardened alloy gear steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.46, 0.50, 0.56),
|
||||
),
|
||||
"electrical_steel": scad.make_material_rmaterial(
|
||||
material_id="laminated_electrical_steel",
|
||||
name="Laminated electrical steel",
|
||||
density=7.65e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.20, 0.27, 0.35),
|
||||
),
|
||||
"copper": scad.make_material_rmaterial(
|
||||
material_id="enameled_copper",
|
||||
name="Enameled copper winding",
|
||||
density=8.96e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.78, 0.27, 0.06),
|
||||
),
|
||||
"magnet": scad.make_material_rmaterial(
|
||||
material_id="ndfeb_n42sh",
|
||||
name="NdFeB N42SH magnet",
|
||||
density=7.50e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.14, 0.34, 0.75),
|
||||
),
|
||||
"pcb": scad.make_material_rmaterial(
|
||||
material_id="fr4_copper_laminate",
|
||||
name="FR-4 copper laminate",
|
||||
density=1.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.03, 0.42, 0.16),
|
||||
),
|
||||
"terminal": scad.make_material_rmaterial(
|
||||
material_id="high_temperature_terminal_polymer",
|
||||
name="High-temperature connector polymer",
|
||||
density=1.45e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.88, 0.70, 0.16),
|
||||
),
|
||||
}
|
||||
print("materials: " + ",".join(sorted(materials)))
|
||||
return materials
|
||||
@@ -0,0 +1,378 @@
|
||||
"""True 12-slot/14-pole BLDC stator and direct-drive rotor assemblies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
try:
|
||||
from .common import (
|
||||
apply_tags,
|
||||
connector_ref,
|
||||
ground_constraint_report,
|
||||
make_annulus_rsolid,
|
||||
make_axis_part_rpart,
|
||||
make_z_rotation_rplacement,
|
||||
radial_centers,
|
||||
)
|
||||
from .dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CLEARANCE_FACTOR,
|
||||
GEAR_HEIGHT,
|
||||
HELIX_ANGLE,
|
||||
MOTOR_MAGNET_OUTER_RADIUS,
|
||||
MOTOR_MAGNET_TANGENTIAL_WIDTH,
|
||||
MOTOR_POLE_COUNT,
|
||||
MOTOR_ROTOR_BACKIRON_RADIUS,
|
||||
MOTOR_ROTOR_BOTTOM_Z,
|
||||
MOTOR_ROTOR_TOP_Z,
|
||||
MOTOR_SHAFT_RADIUS,
|
||||
MOTOR_SHELL_INNER_RADIUS,
|
||||
MOTOR_SLOT_COUNT,
|
||||
MOTOR_STATOR_BOTTOM_Z,
|
||||
MOTOR_STATOR_OUTER_RADIUS,
|
||||
MOTOR_STATOR_TOOTH_INNER_RADIUS,
|
||||
MOTOR_STATOR_TOOTH_WIDTH,
|
||||
MOTOR_STATOR_TOP_Z,
|
||||
MOTOR_STATOR_YOKE_INNER_RADIUS,
|
||||
PRESSURE_ANGLE,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
STAGE_1,
|
||||
)
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
from common import (
|
||||
apply_tags,
|
||||
connector_ref,
|
||||
ground_constraint_report,
|
||||
make_annulus_rsolid,
|
||||
make_axis_part_rpart,
|
||||
make_z_rotation_rplacement,
|
||||
radial_centers,
|
||||
)
|
||||
from dimensions import (
|
||||
ADDENDUM_FACTOR,
|
||||
BACKLASH,
|
||||
CLEARANCE_FACTOR,
|
||||
GEAR_HEIGHT,
|
||||
HELIX_ANGLE,
|
||||
MOTOR_MAGNET_OUTER_RADIUS,
|
||||
MOTOR_MAGNET_TANGENTIAL_WIDTH,
|
||||
MOTOR_POLE_COUNT,
|
||||
MOTOR_ROTOR_BACKIRON_RADIUS,
|
||||
MOTOR_ROTOR_BOTTOM_Z,
|
||||
MOTOR_ROTOR_TOP_Z,
|
||||
MOTOR_SHAFT_RADIUS,
|
||||
MOTOR_SHELL_INNER_RADIUS,
|
||||
MOTOR_SLOT_COUNT,
|
||||
MOTOR_STATOR_BOTTOM_Z,
|
||||
MOTOR_STATOR_OUTER_RADIUS,
|
||||
MOTOR_STATOR_TOOTH_INNER_RADIUS,
|
||||
MOTOR_STATOR_TOOTH_WIDTH,
|
||||
MOTOR_STATOR_TOP_Z,
|
||||
MOTOR_STATOR_YOKE_INNER_RADIUS,
|
||||
PRESSURE_ANGLE,
|
||||
REAR_BEARING_CENTER_Z,
|
||||
STAGE_1,
|
||||
)
|
||||
|
||||
|
||||
def make_bldc_stator_rassembly(
|
||||
*,
|
||||
steel_material: scad.Material,
|
||||
copper_material: scad.Material,
|
||||
) -> scad.Assembly:
|
||||
"""Build a laminated 12-slot stator and twelve fixed winding packs."""
|
||||
|
||||
core = _make_stator_core_rpart(material=steel_material)
|
||||
winding = _make_winding_pack_rpart(material=copper_material)
|
||||
stator = scad.make_assembly_rassembly(
|
||||
assembly_id="bldc_12_slot_stator",
|
||||
name="12-slot laminated stator with discrete copper slot packs",
|
||||
)
|
||||
stator = scad.add_component_rassembly(
|
||||
assembly=stator,
|
||||
item=core,
|
||||
component_id="stator_core",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Laminated stator core",
|
||||
)
|
||||
stator = scad.ground_component_rassembly(assembly=stator, component_id="stator_core")
|
||||
for index, angle, _center in radial_centers(
|
||||
count=MOTOR_SLOT_COUNT,
|
||||
radius=0.0,
|
||||
angle_offset=0.0,
|
||||
):
|
||||
component_id = f"winding_{index + 1:02d}"
|
||||
stator = scad.add_component_rassembly(
|
||||
assembly=stator,
|
||||
item=winding,
|
||||
component_id=component_id,
|
||||
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
|
||||
name=f"Slot winding pack {index + 1}",
|
||||
)
|
||||
stator = scad.add_fixed_constraint_rassembly(
|
||||
assembly=stator,
|
||||
constraint_id=f"{component_id}_potted_to_core",
|
||||
connector_a=connector_ref(component_id="stator_core", connector_id=component_id),
|
||||
connector_b=connector_ref(component_id=component_id, connector_id="mount_axis"),
|
||||
name=f"Winding {index + 1} varnish and potting retention",
|
||||
)
|
||||
stator = scad.forward_connector_rassembly(
|
||||
assembly=stator,
|
||||
connector_id="shell_axis",
|
||||
source_component_id="stator_core",
|
||||
source_connector_id="shell_axis",
|
||||
name="Stator press-fit axis",
|
||||
offset=None,
|
||||
)
|
||||
stator = scad.solve_assembly_constraints_rassembly(assembly=stator, strict=True)
|
||||
ground_constraint_report(label="stator", assembly=stator)
|
||||
return stator
|
||||
|
||||
|
||||
def make_bldc_rotor_rassembly(
|
||||
*,
|
||||
steel_material: scad.Material,
|
||||
magnet_material: scad.Material,
|
||||
) -> scad.Assembly:
|
||||
"""Build the rotor, bonded magnets, shaft, and integrated stage-1 sun."""
|
||||
|
||||
core = _make_rotor_shaft_sun_rpart(material=steel_material)
|
||||
magnet = _make_rotor_magnet_rpart(material=magnet_material)
|
||||
rotor = scad.make_assembly_rassembly(
|
||||
assembly_id="direct_coupled_bldc_rotor",
|
||||
name="14-pole BLDC rotor with integrated stage-1 sun shaft",
|
||||
)
|
||||
rotor = scad.add_component_rassembly(
|
||||
assembly=rotor,
|
||||
item=core,
|
||||
component_id="rotor_core_shaft_sun",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Rotor back iron, shaft, and stage-1 sun",
|
||||
)
|
||||
rotor = scad.ground_component_rassembly(assembly=rotor, component_id="rotor_core_shaft_sun")
|
||||
for index, angle, _center in radial_centers(count=MOTOR_POLE_COUNT, radius=0.0):
|
||||
component_id = f"magnet_{index + 1:02d}"
|
||||
rotor = scad.add_component_rassembly(
|
||||
assembly=rotor,
|
||||
item=magnet,
|
||||
component_id=component_id,
|
||||
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
|
||||
name=f"Bonded rotor magnet {index + 1}",
|
||||
)
|
||||
rotor = scad.add_fixed_constraint_rassembly(
|
||||
assembly=rotor,
|
||||
constraint_id=f"{component_id}_bonded_to_rotor",
|
||||
connector_a=connector_ref(component_id="rotor_core_shaft_sun", connector_id=component_id),
|
||||
connector_b=connector_ref(component_id=component_id, connector_id="bond_axis"),
|
||||
name=f"Magnet {index + 1} adhesive and sleeve retention",
|
||||
)
|
||||
for connector_id in (
|
||||
"rotor_axis",
|
||||
"rear_bearing_axis",
|
||||
"front_bearing_axis",
|
||||
"stage1_sun_axis",
|
||||
):
|
||||
rotor = scad.forward_connector_rassembly(
|
||||
assembly=rotor,
|
||||
connector_id=connector_id,
|
||||
source_component_id="rotor_core_shaft_sun",
|
||||
source_connector_id=connector_id,
|
||||
name=connector_id.replace("_", " "),
|
||||
offset=None,
|
||||
)
|
||||
rotor = scad.solve_assembly_constraints_rassembly(assembly=rotor, strict=True)
|
||||
ground_constraint_report(label="rotor", assembly=rotor)
|
||||
print(
|
||||
f"rotor_direct_coupling: shaft_d={MOTOR_SHAFT_RADIUS * 2.0:.1f} "
|
||||
f"magnets={MOTOR_POLE_COUNT} stage1_sun_teeth={STAGE_1.sun_teeth}"
|
||||
)
|
||||
return rotor
|
||||
|
||||
|
||||
def _make_stator_core_rpart(*, material: scad.Material) -> scad.Part:
|
||||
yoke = make_annulus_rsolid(
|
||||
outer_radius=MOTOR_STATOR_OUTER_RADIUS,
|
||||
inner_radius=MOTOR_STATOR_YOKE_INNER_RADIUS,
|
||||
bottom_z=MOTOR_STATOR_BOTTOM_Z,
|
||||
height=MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z,
|
||||
tags=("role.stator_back_iron",),
|
||||
)
|
||||
tooth_length = MOTOR_STATOR_YOKE_INNER_RADIUS - MOTOR_STATOR_TOOTH_INNER_RADIUS + 0.40
|
||||
tooth_center_radius = MOTOR_STATOR_TOOTH_INNER_RADIUS + tooth_length / 2.0
|
||||
teeth = []
|
||||
for _index, angle, _center in radial_centers(count=MOTOR_SLOT_COUNT, radius=0.0):
|
||||
tooth = scad.make_box_rsolid(
|
||||
width=tooth_length,
|
||||
height=MOTOR_STATOR_TOOTH_WIDTH,
|
||||
depth=MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z,
|
||||
bottom_face_center=(tooth_center_radius, 0.0, MOTOR_STATOR_BOTTOM_Z),
|
||||
)
|
||||
teeth.append(
|
||||
scad.rotate_shape(
|
||||
shape=tooth,
|
||||
angle=angle,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, 0.0),
|
||||
)
|
||||
)
|
||||
core = scad.union_rsolid(yoke, teeth, glue=False)
|
||||
core = apply_tags(
|
||||
shape=core,
|
||||
tags=("role.stator_core", "role.stator_thermal_path", "group.bldc_motor"),
|
||||
)
|
||||
connectors = [
|
||||
(
|
||||
"shell_axis",
|
||||
(0.0, 0.0, (MOTOR_STATOR_BOTTOM_Z + MOTOR_STATOR_TOP_Z) / 2.0),
|
||||
"Stator press-fit axis",
|
||||
)
|
||||
]
|
||||
part = make_axis_part_rpart(
|
||||
part_id="stator_core",
|
||||
body=core,
|
||||
name="12-slot laminated electrical-steel stator stack",
|
||||
material=material,
|
||||
connectors=connectors,
|
||||
)
|
||||
for index, angle, _center in radial_centers(
|
||||
count=MOTOR_SLOT_COUNT,
|
||||
radius=0.0,
|
||||
angle_offset=0.0,
|
||||
):
|
||||
rotation = make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle)
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=f"winding_{index + 1:02d}",
|
||||
placement=rotation,
|
||||
name=f"Slot winding {index + 1} retention datum",
|
||||
)
|
||||
part = scad.add_connector_rpart(part=part, connector=connector)
|
||||
print(
|
||||
f"stator_core_geometry: slots={MOTOR_SLOT_COUNT} active_length="
|
||||
f"{MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z:.1f} radial_fit_clearance="
|
||||
f"{MOTOR_SHELL_INNER_RADIUS - MOTOR_STATOR_OUTER_RADIUS:.2f}"
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
def _make_winding_pack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
side_depth = MOTOR_STATOR_TOP_Z - MOTOR_STATOR_BOTTOM_Z + 0.4
|
||||
side_bottom_z = MOTOR_STATOR_BOTTOM_Z - 0.2
|
||||
side_positive = scad.make_box_rsolid(
|
||||
width=4.2,
|
||||
height=1.2,
|
||||
depth=side_depth,
|
||||
bottom_face_center=(17.55, 2.1, side_bottom_z),
|
||||
)
|
||||
side_negative = scad.make_box_rsolid(
|
||||
width=4.2,
|
||||
height=1.2,
|
||||
depth=side_depth,
|
||||
bottom_face_center=(17.55, -2.1, side_bottom_z),
|
||||
)
|
||||
rear_end_turn = scad.make_box_rsolid(
|
||||
width=4.2,
|
||||
height=5.4,
|
||||
depth=0.9,
|
||||
bottom_face_center=(17.55, 0.0, MOTOR_STATOR_BOTTOM_Z - 1.0),
|
||||
)
|
||||
front_end_turn = scad.make_box_rsolid(
|
||||
width=4.2,
|
||||
height=5.4,
|
||||
depth=0.9,
|
||||
bottom_face_center=(17.55, 0.0, MOTOR_STATOR_TOP_Z + 0.1),
|
||||
)
|
||||
winding = scad.union_rsolid(
|
||||
side_positive,
|
||||
side_negative,
|
||||
rear_end_turn,
|
||||
front_end_turn,
|
||||
glue=False,
|
||||
)
|
||||
winding = apply_tags(
|
||||
shape=winding,
|
||||
tags=("role.copper_slot_winding", "group.three_phase_windings"),
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="reusable_slot_winding",
|
||||
body=winding,
|
||||
name="Reusable four-segment copper tooth winding pack",
|
||||
material=material,
|
||||
connectors=(("mount_axis", (0.0, 0.0, 0.0), "Core potting datum"),),
|
||||
)
|
||||
|
||||
|
||||
def _make_rotor_shaft_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
shaft_bottom_z = REAR_BEARING_CENTER_Z - 3.0
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=MOTOR_SHAFT_RADIUS,
|
||||
height=STAGE_1.top_z - shaft_bottom_z,
|
||||
bottom_face_center=(0.0, 0.0, shaft_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
back_iron = scad.make_cylinder_rsolid(
|
||||
radius=MOTOR_ROTOR_BACKIRON_RADIUS,
|
||||
height=MOTOR_ROTOR_TOP_Z - MOTOR_ROTOR_BOTTOM_Z,
|
||||
bottom_face_center=(0.0, 0.0, MOTOR_ROTOR_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=STAGE_1.sun_teeth,
|
||||
module=STAGE_1.module,
|
||||
pressure_angle=PRESSURE_ANGLE,
|
||||
helix_angle=HELIX_ANGLE,
|
||||
gear_height=GEAR_HEIGHT,
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
sun = scad.translate_shape(shape=sun, vector=(0.0, 0.0, STAGE_1.bottom_z))
|
||||
rotor = scad.union_rsolid(shaft, back_iron, sun, glue=False)
|
||||
rotor = apply_tags(
|
||||
shape=rotor,
|
||||
tags=("role.rotor_back_iron", "role.direct_drive_shaft", "role.stage1.sun_gear", "group.bldc_motor"),
|
||||
)
|
||||
part = make_axis_part_rpart(
|
||||
part_id="rotor_core_shaft_sun",
|
||||
body=rotor,
|
||||
name="Integrated rotor back iron, 8 mm shaft, and stage-1 sun",
|
||||
material=material,
|
||||
connectors=(
|
||||
("rotor_axis", (0.0, 0.0, -2.5), "Motor rotation axis"),
|
||||
("rear_bearing_axis", (0.0, 0.0, REAR_BEARING_CENTER_Z), "Rear motor bearing shaft seat"),
|
||||
("front_bearing_axis", (0.0, 0.0, -2.5), "Front motor bearing shaft seat"),
|
||||
("stage1_sun_axis", (0.0, 0.0, STAGE_1.mid_z), "Integrated stage-1 sun axis"),
|
||||
),
|
||||
)
|
||||
for index, angle, _center in radial_centers(count=MOTOR_POLE_COUNT, radius=0.0):
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=f"magnet_{index + 1:02d}",
|
||||
placement=make_z_rotation_rplacement(origin=(0.0, 0.0, 0.0), angle_degrees=angle),
|
||||
name=f"Magnet {index + 1} bond datum",
|
||||
)
|
||||
part = scad.add_connector_rpart(part=part, connector=connector)
|
||||
return part
|
||||
|
||||
|
||||
def _make_rotor_magnet_rpart(*, material: scad.Material) -> scad.Part:
|
||||
half_width = MOTOR_MAGNET_TANGENTIAL_WIDTH / 2.0
|
||||
outer_x = (MOTOR_MAGNET_OUTER_RADIUS**2 - half_width**2) ** 0.5
|
||||
radial_depth = outer_x - MOTOR_ROTOR_BACKIRON_RADIUS + 0.05
|
||||
magnet = scad.make_box_rsolid(
|
||||
width=radial_depth,
|
||||
height=MOTOR_MAGNET_TANGENTIAL_WIDTH,
|
||||
depth=MOTOR_ROTOR_TOP_Z - MOTOR_ROTOR_BOTTOM_Z,
|
||||
bottom_face_center=(outer_x - radial_depth / 2.0, 0.0, MOTOR_ROTOR_BOTTOM_Z),
|
||||
)
|
||||
magnet = apply_tags(shape=magnet, tags=("role.rotor_magnet", "group.rotor_magnets"))
|
||||
print(
|
||||
f"rotor_magnet_envelope: corner_radius={MOTOR_MAGNET_OUTER_RADIUS:.2f} "
|
||||
f"air_gap={MOTOR_STATOR_TOOTH_INNER_RADIUS - MOTOR_MAGNET_OUTER_RADIUS:.2f}"
|
||||
)
|
||||
return make_axis_part_rpart(
|
||||
part_id="reusable_rotor_magnet",
|
||||
body=magnet,
|
||||
name="Reusable bonded NdFeB rotor magnet",
|
||||
material=material,
|
||||
connectors=(("bond_axis", (0.0, 0.0, 0.0), "Rotor bond datum"),),
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
# SimpleCADAPI Examples
|
||||
|
||||
Run examples from the repository root with `uv run python <path>`.
|
||||
Generated STEP/STL/JSON files are written to `examples/out/`, which is ignored by git.
|
||||
|
||||
## Examples
|
||||
|
||||
- `01_basic_modeling.py` — functional shape modeling, booleans, and STEP/STL export.
|
||||
- `02_graph_replay.py` — `GraphSession`, canonical model JSON export, and replay.
|
||||
- `03_expressions.py` — expression parameters captured in a replayable model graph.
|
||||
- `05_loft_sweep_revolve.py` — profile operations: revolve, loft, and sweep.
|
||||
- `06_parametric_gear_model.py` — lightweight involute spur gear model JSON example for replay/export tests.
|
||||
- `07_serialization_operation_tree.py` — compact serialization demo showing how source calls map to canonical operation-tree nodes, including expressions, primitive lowering, features, booleans, transforms, patterns, and detail operations.
|
||||
- `13_cycloidal_reducer.py` — compact 50 mm diameter, 10 mm tall, 10:1 cycloidal reducer assembly with twin segmented B-spline cycloidal discs, 180-degree opposed input eccentric cams, 18-degree half-lobe tooth-index phase, three-hole input/output disks, and assembly constraints.
|
||||
- `14_ball_bearing.py` — parameterized ball bearing standard assembly with grooved inner/outer race rings, direct sphere rolling elements, stable ring component IDs, ring axis connectors, an inner-to-outer revolute constraint, and a demo shaft/housing bound through those connectors.
|
||||
- `15_cached_mesh_obj_export.py` — developer-facing cached-mesh example that builds a normal Solid, bypasses the public STL exporter, reads the internal mesh cache, and writes a Wavefront OBJ file.
|
||||
- `16_compact_two_stage_planetary_reducer/` — modular 58.8 mm diameter, 30 mm tall, 20:1 two-stage herringbone planetary reducer with through-bolted actuator housing bosses, sealed input/output end caps, realistic output register pads, reusable stdlib ball bearing placements, graph/model JSON replay, STEP export, solved gear constraints, and a `collision_probe.py` static verifier run.
|
||||
- `17_static_collision_verifier.py` — static current-pose verifier example using internal cached meshes and python-fcl to report over-tolerance contact penetration.
|
||||
- `18_leg_wheel_robot_dog_leg/` — planar leg-wheel module using three reused reducer actuator modules, a fixed motor-can part, a compact coaxial thigh/knee-drive actuator stack, a thigh output-flange-bolted upper link, a coaxial knee-drive output crank with 6-hole output flange pattern, a true parallelogram pushrod linkage whose knee-side `BB'` ear is integrated into the shank plate, knee bearing retainer holes, wheel-hub housing/output bolt circles, graph/model JSON replay, STEP/FCStd export, and a leg-level `collision_probe.py` packaging check.
|
||||
- `19_four_planet_planetary_reducer/` — exposed single-stage 3.5:1 fixed-ring planetary gearset with one input sun gear, four equally spaced planet gears, an internal ring gear, a four-pin output carrier, solved revolute/external gear/internal belt-equivalent mesh constraints, graph/model JSON replay, STEP export, and FCStd export.
|
||||
- `20_five_axis_desktop_robot_arm/` — five-revolute-axis desktop robot arm inspired by the reference image, using five reused Example 16 reducer actuator modules with an improved rear-service motor package, explicit part/interface validation before assembly, base yaw, shoulder/elbow/wrist pitch, tool roll, bolted housing/output flange interfaces, sensor face detail, graph/model JSON replay, STEP export, and FCStd export.
|
||||
- `20_integrated_bldc_joint_actuator/` — compact 50 mm OD joint actuator with a real 12-slot/14-pole inner-rotor BLDC motor, integrated rotor-shaft/stage-1 sun, 20:1 two-stage herringbone planetary reducer, serviceable split housing, paired output bearings, circular ESC PCB, rear phase and power/CAN terminals, graph/model JSON replay, STEP export, and FCStd export.
|
||||
Reference in New Issue
Block a user