feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -4,3 +4,8 @@ out/
|
||||
*.json
|
||||
*.FCStd
|
||||
*.FCBak
|
||||
*.fcstd
|
||||
*.fcbak
|
||||
*.scene.zip
|
||||
*.zip
|
||||
*.log
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""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")
|
||||
@@ -1,39 +0,0 @@
|
||||
"""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")
|
||||
@@ -1,33 +0,0 @@
|
||||
"""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,108 @@
|
||||
"""Declare, propagate, validate, and serialize a dimension tolerance chain.
|
||||
|
||||
Run from the repository root with:
|
||||
uv run python examples/04_dimension_tolerance_chain.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
OUT = Path("examples/out")
|
||||
|
||||
|
||||
@scad.model(graph_id="dimension_tolerance_chain")
|
||||
def build_model():
|
||||
housing_span = scad.var(
|
||||
name="housing_span",
|
||||
default=100.0,
|
||||
unit="mm",
|
||||
tolerance=0.15,
|
||||
comment="Internal housing span",
|
||||
)
|
||||
bearing_width = scad.var(
|
||||
name="bearing_width",
|
||||
default=2.0,
|
||||
unit="cm",
|
||||
tolerance=(-0.04, 0.05),
|
||||
tolerance_unit="mm",
|
||||
comment="Bearing width",
|
||||
)
|
||||
spacer_width = scad.var(
|
||||
name="spacer_width",
|
||||
default=79.4,
|
||||
unit="mm",
|
||||
tolerance=0.05,
|
||||
comment="Spacer width",
|
||||
)
|
||||
axial_clearance = housing_span - bearing_width - spacer_width
|
||||
|
||||
worst_case = scad.analyze_tolerance(
|
||||
value=axial_clearance,
|
||||
method="worst_case",
|
||||
)
|
||||
rss = scad.analyze_tolerance(value=axial_clearance, method="rss")
|
||||
|
||||
housing = scad.make_box_rsolid(
|
||||
width=housing_span,
|
||||
height=10.0,
|
||||
depth=10.0,
|
||||
tag_prefix="tolerance_chain.housing",
|
||||
result_tag="part.tolerance_chain.housing",
|
||||
)
|
||||
session = scad.get_active_session()
|
||||
if session is None:
|
||||
raise RuntimeError("dimension tolerance model has no active session")
|
||||
session.require_tolerance(
|
||||
value=axial_clearance,
|
||||
tolerance=(-0.25, 0.24),
|
||||
tolerance_unit="mm",
|
||||
method="worst_case",
|
||||
name="axial_clearance",
|
||||
)
|
||||
report = session.validate_tolerances(raise_on_failure=True)
|
||||
scad.capture_result(value=housing)
|
||||
return {
|
||||
"housing": housing,
|
||||
"worst_case": worst_case,
|
||||
"rss": rss,
|
||||
"report": report,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
result = build_model()
|
||||
report_data = result.value
|
||||
(OUT / "dimension_tolerance_chain.model.json").write_text(
|
||||
result.model_json,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
worst_case = report_data["worst_case"]
|
||||
rss = report_data["rss"]
|
||||
print("housing_volume", round(report_data["housing"].get_volume(), 3))
|
||||
print(
|
||||
"worst_case",
|
||||
round(worst_case.nominal, 3),
|
||||
round(worst_case.lower_bound, 3),
|
||||
round(worst_case.upper_bound, 3),
|
||||
)
|
||||
print("result_unit", worst_case.dimension.name, worst_case.unit.symbol)
|
||||
print(
|
||||
"rss",
|
||||
round(rss.nominal, 3),
|
||||
round(rss.lower_bound, 3),
|
||||
round(rss.upper_bound, 3),
|
||||
)
|
||||
print("requirements_passed", report_data["report"].passed)
|
||||
print(
|
||||
"serialized_tolerance_graph",
|
||||
"tolerance_graph" in json.loads(result.model_json),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
"""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))
|
||||
@@ -1,192 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,312 +0,0 @@
|
||||
"""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)))
|
||||
@@ -21,7 +21,6 @@ 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"
|
||||
@@ -30,7 +29,7 @@ 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,
|
||||
sketch=sketch,
|
||||
require_fully_constrained=True,
|
||||
)
|
||||
points = sorted(
|
||||
@@ -54,228 +53,513 @@ def _solve_and_report(name: str, sketch: scad.Sketch) -> None:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _promote_face(name: str, sketch: scad.Sketch):
|
||||
_solve_and_report(name, sketch)
|
||||
_solve_and_report(name=name, sketch=sketch)
|
||||
return scad.make_face_from_sketch_rface(
|
||||
sketch,
|
||||
sketch=sketch,
|
||||
require_fully_constrained=True,
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_rect_profile(name, x0, y0, width, height):
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
sketch = scad.make_sketch_rsketch(name=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_point_rsketch(sketch=sketch, point_id="p0", x=x0, y=y0)
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id="p1",
|
||||
x=x0 + width,
|
||||
y=y0,
|
||||
)
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id="p2",
|
||||
x=x0 + width,
|
||||
y=y0 + height,
|
||||
)
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id="p3",
|
||||
x=x0,
|
||||
y=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.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="bottom",
|
||||
start="p0",
|
||||
end="p1",
|
||||
)
|
||||
sketch = scad.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="right",
|
||||
start="p1",
|
||||
end="p2",
|
||||
)
|
||||
sketch = scad.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="top",
|
||||
start="p2",
|
||||
end="p3",
|
||||
)
|
||||
sketch = scad.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="left",
|
||||
start="p3",
|
||||
end="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)
|
||||
sketch = scad.constrain_horizontal_rsketch(sketch=sketch, line="bottom")
|
||||
sketch = scad.constrain_vertical_rsketch(sketch=sketch, line="right")
|
||||
sketch = scad.constrain_parallel_rsketch(
|
||||
sketch=sketch,
|
||||
a="bottom",
|
||||
b="top",
|
||||
)
|
||||
sketch = scad.constrain_parallel_rsketch(
|
||||
sketch=sketch,
|
||||
a="left",
|
||||
b="right",
|
||||
)
|
||||
sketch = scad.constrain_perpendicular_rsketch(
|
||||
sketch=sketch,
|
||||
a="bottom",
|
||||
b="right",
|
||||
)
|
||||
sketch = scad.constrain_equal_length_rsketch(
|
||||
sketch=sketch,
|
||||
a="bottom",
|
||||
b="top",
|
||||
)
|
||||
sketch = scad.constrain_equal_length_rsketch(
|
||||
sketch=sketch,
|
||||
a="left",
|
||||
b="right",
|
||||
)
|
||||
sketch = scad.constrain_distance_rsketch(
|
||||
sketch=sketch,
|
||||
a="p0",
|
||||
b="p1",
|
||||
value=width,
|
||||
)
|
||||
sketch = scad.constrain_distance_rsketch(
|
||||
sketch=sketch,
|
||||
a="p0",
|
||||
b="p3",
|
||||
value=height,
|
||||
)
|
||||
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="p0")
|
||||
return _promote_face(name=name, sketch=sketch)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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)
|
||||
sketch = scad.make_sketch_rsketch(name=name, plane="XY")
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id="center",
|
||||
x=center_x,
|
||||
y=center_y,
|
||||
)
|
||||
sketch = scad.add_circle_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id=circle_id,
|
||||
center="center",
|
||||
radius=radius,
|
||||
)
|
||||
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="center")
|
||||
sketch = scad.constrain_radius_rsketch(
|
||||
sketch=sketch,
|
||||
circle=circle_id,
|
||||
value=radius,
|
||||
)
|
||||
return _promote_face(name=name, sketch=sketch)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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.make_sketch_rsketch(name=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)
|
||||
for point_id, x, y in (
|
||||
("center", center_x, center_y),
|
||||
("left", center_x - half_w, center_y),
|
||||
("top", center_x, center_y + half_h),
|
||||
("right", center_x + half_w, center_y),
|
||||
("bottom", center_x, center_y - half_h),
|
||||
("guide_upper_start", center_x - half_w, center_y + guide_gap),
|
||||
("guide_upper_end", center_x, center_y + half_h + guide_gap),
|
||||
("guide_lower_start", center_x + half_w, center_y - guide_gap),
|
||||
("guide_lower_end", center_x, center_y - half_h - guide_gap),
|
||||
):
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id=point_id,
|
||||
x=x,
|
||||
y=y,
|
||||
)
|
||||
|
||||
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)
|
||||
for entity_id, start, end, construction in (
|
||||
("bottom_left", "left", "bottom", False),
|
||||
("right_bottom", "bottom", "right", False),
|
||||
("top_right", "right", "top", False),
|
||||
("left_top", "top", "left", False),
|
||||
("guide_upper", "guide_upper_start", "guide_upper_end", True),
|
||||
("guide_lower", "guide_lower_start", "guide_lower_end", True),
|
||||
):
|
||||
sketch = scad.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id=entity_id,
|
||||
start=start,
|
||||
end=end,
|
||||
construction=construction,
|
||||
)
|
||||
|
||||
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=sketch, target="center")
|
||||
for function, a, b, value in (
|
||||
(scad.constrain_distance_x_rsketch, "left", "center", half_w),
|
||||
(scad.constrain_distance_y_rsketch, "left", "center", 0.0),
|
||||
(scad.constrain_distance_x_rsketch, "center", "right", half_w),
|
||||
(scad.constrain_distance_y_rsketch, "center", "right", 0.0),
|
||||
(scad.constrain_distance_x_rsketch, "center", "top", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "center", "top", half_h),
|
||||
(scad.constrain_distance_x_rsketch, "bottom", "center", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "bottom", "center", half_h),
|
||||
):
|
||||
sketch = function(sketch=sketch, a=a, b=b, value=value)
|
||||
|
||||
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)
|
||||
for function, a, b in (
|
||||
(scad.constrain_parallel_rsketch, "left_top", "right_bottom"),
|
||||
(scad.constrain_parallel_rsketch, "top_right", "bottom_left"),
|
||||
(scad.constrain_equal_length_rsketch, "left_top", "top_right"),
|
||||
(scad.constrain_equal_length_rsketch, "top_right", "right_bottom"),
|
||||
(scad.constrain_equal_length_rsketch, "right_bottom", "bottom_left"),
|
||||
):
|
||||
sketch = function(sketch=sketch, a=a, b=b)
|
||||
|
||||
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")
|
||||
for function, a, b, value in (
|
||||
(scad.constrain_distance_x_rsketch, "left", "guide_upper_start", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "left", "guide_upper_start", guide_gap),
|
||||
(scad.constrain_distance_x_rsketch, "top", "guide_upper_end", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "top", "guide_upper_end", guide_gap),
|
||||
(scad.constrain_distance_x_rsketch, "guide_lower_start", "right", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "guide_lower_start", "right", guide_gap),
|
||||
(scad.constrain_distance_x_rsketch, "guide_lower_end", "bottom", 0.0),
|
||||
(scad.constrain_distance_y_rsketch, "guide_lower_end", "bottom", guide_gap),
|
||||
):
|
||||
sketch = function(sketch=sketch, a=a, b=b, value=value)
|
||||
|
||||
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)
|
||||
for function, a, b in (
|
||||
(scad.constrain_parallel_rsketch, "guide_upper", "guide_lower"),
|
||||
(scad.constrain_parallel_rsketch, "guide_upper", "right_bottom"),
|
||||
(scad.constrain_parallel_rsketch, "guide_lower", "left_top"),
|
||||
(scad.constrain_equal_length_rsketch, "guide_upper", "right_bottom"),
|
||||
(scad.constrain_equal_length_rsketch, "guide_lower", "left_top"),
|
||||
):
|
||||
sketch = function(sketch=sketch, a=a, b=b)
|
||||
return _promote_face(name=name, sketch=sketch)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_curve_guided_relief_profile(name, center_x, center_y, radius, guide_span):
|
||||
sketch = scad.make_sketch_rsketch(name, plane="XY")
|
||||
sketch = scad.make_sketch_rsketch(name=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)
|
||||
for point_id, x, y in (
|
||||
("center", center_x, center_y),
|
||||
("rim", center_x + radius, center_y),
|
||||
("clearance_center", center_x, center_y),
|
||||
("upper_left", center_x - guide_span, center_y + radius),
|
||||
("upper_right", center_x + guide_span, center_y + radius),
|
||||
("lower_left", center_x - guide_span, center_y - radius),
|
||||
("lower_right", center_x + guide_span, center_y - radius),
|
||||
):
|
||||
sketch = scad.add_point_rsketch(
|
||||
sketch=sketch,
|
||||
point_id=point_id,
|
||||
x=x,
|
||||
y=y,
|
||||
)
|
||||
|
||||
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.add_circle_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="relief",
|
||||
center="center",
|
||||
radius=radius,
|
||||
)
|
||||
sketch = scad.add_circle_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id="clearance",
|
||||
center="clearance_center",
|
||||
radius=radius,
|
||||
construction=True,
|
||||
)
|
||||
for entity_id, start, end in (
|
||||
("radius_probe", "center", "rim"),
|
||||
("upper_rail", "upper_left", "upper_right"),
|
||||
("lower_rail", "lower_left", "lower_right"),
|
||||
):
|
||||
sketch = scad.add_line_rsketch(
|
||||
sketch=sketch,
|
||||
entity_id=entity_id,
|
||||
start=start,
|
||||
end=end,
|
||||
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_fix_rsketch(sketch=sketch, target="center")
|
||||
sketch = scad.constrain_radius_rsketch(
|
||||
sketch=sketch,
|
||||
circle="relief",
|
||||
value=radius,
|
||||
)
|
||||
sketch = scad.constrain_point_on_rsketch(
|
||||
sketch=sketch,
|
||||
point="rim",
|
||||
entity="relief",
|
||||
)
|
||||
sketch = scad.constrain_horizontal_rsketch(
|
||||
sketch=sketch,
|
||||
line="radius_probe",
|
||||
)
|
||||
sketch = scad.constrain_length_rsketch(
|
||||
sketch=sketch,
|
||||
line="radius_probe",
|
||||
value=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_concentric_rsketch(
|
||||
sketch=sketch,
|
||||
a="relief",
|
||||
b="clearance",
|
||||
)
|
||||
sketch = scad.constrain_equal_radius_rsketch(
|
||||
sketch=sketch,
|
||||
a="relief",
|
||||
b="clearance",
|
||||
)
|
||||
sketch = scad.constrain_horizontal_rsketch(
|
||||
sketch=sketch,
|
||||
line="upper_rail",
|
||||
)
|
||||
sketch = scad.constrain_horizontal_rsketch(
|
||||
sketch=sketch,
|
||||
line="lower_rail",
|
||||
)
|
||||
sketch = scad.constrain_tangent_rsketch(
|
||||
sketch=sketch,
|
||||
a="upper_rail",
|
||||
b="relief",
|
||||
)
|
||||
sketch = scad.constrain_tangent_rsketch(
|
||||
sketch=sketch,
|
||||
a="lower_rail",
|
||||
b="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)
|
||||
for a, b, value in (
|
||||
("center", "upper_left", -guide_span),
|
||||
("center", "upper_right", guide_span),
|
||||
("center", "lower_left", -guide_span),
|
||||
("center", "lower_right", guide_span),
|
||||
):
|
||||
sketch = scad.constrain_distance_x_rsketch(
|
||||
sketch=sketch,
|
||||
a=a,
|
||||
b=b,
|
||||
value=value,
|
||||
)
|
||||
return _promote_face(name=name, sketch=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")
|
||||
@scad.model(graph_id="constrained_sketch")
|
||||
def build_model():
|
||||
plate_w = scad.var(name="plate_w", default=96.0, comment="plate width")
|
||||
plate_h = scad.var(name="plate_h", default=54.0, comment="plate height")
|
||||
plate_t = scad.var(name="plate_t", default=6.0, comment="plate thickness")
|
||||
boss_r = scad.var(
|
||||
name="boss_r",
|
||||
default=14.0,
|
||||
comment="raised center boss radius",
|
||||
)
|
||||
boss_h = scad.var(
|
||||
name="boss_h",
|
||||
default=5.0,
|
||||
comment="raised center boss height",
|
||||
)
|
||||
bore_r = scad.var(name="bore_r", default=5.0, comment="through bore radius")
|
||||
mount_r = scad.var(
|
||||
name="mount_r",
|
||||
default=3.0,
|
||||
comment="mounting hole radius",
|
||||
)
|
||||
margin_x = scad.var(
|
||||
name="mount_margin_x",
|
||||
default=12.0,
|
||||
comment="mounting hole x margin",
|
||||
)
|
||||
margin_y = scad.var(
|
||||
name="mount_margin_y",
|
||||
default=9.0,
|
||||
comment="mounting hole y margin",
|
||||
)
|
||||
slot_w = scad.var(name="slot_w", default=34.0, comment="service slot width")
|
||||
slot_h = scad.var(name="slot_h", default=8.0, comment="service slot height")
|
||||
slot_y = scad.var(
|
||||
name="slot_center_y",
|
||||
default=16.0,
|
||||
comment="service slot center y",
|
||||
)
|
||||
diamond_w = scad.var(
|
||||
name="guided_diamond_w",
|
||||
default=14.0,
|
||||
comment="guided diamond pocket width",
|
||||
)
|
||||
diamond_h = scad.var(
|
||||
name="guided_diamond_h",
|
||||
default=8.0,
|
||||
comment="guided diamond pocket height",
|
||||
)
|
||||
diamond_guide_gap = scad.var(
|
||||
name="guided_diamond_guide_gap",
|
||||
default=5.0,
|
||||
comment="parallel guide rail offset",
|
||||
)
|
||||
relief_r = scad.var(
|
||||
name="curve_relief_r",
|
||||
default=4.0,
|
||||
comment="curve-guided relief radius",
|
||||
)
|
||||
relief_guide_span = scad.var(
|
||||
name="curve_relief_guide_span",
|
||||
default=9.0,
|
||||
comment="curve relief construction rail half span",
|
||||
)
|
||||
|
||||
center_x = plate_w / 2.0
|
||||
center_y = plate_h / 2.0
|
||||
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")
|
||||
plate_profile = make_rect_profile(
|
||||
name="plate_outline",
|
||||
x0=0.0,
|
||||
y0=0.0,
|
||||
width=plate_w,
|
||||
height=plate_h,
|
||||
)
|
||||
plate_profile = scad.apply_tag(
|
||||
shape=plate_profile,
|
||||
tag="demo.profile.plate",
|
||||
)
|
||||
plate = scad.extrude_rsolid(
|
||||
profile=plate_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t,
|
||||
tag_prefix="constrained_sketch.plate",
|
||||
result_tag="part.constrained_sketch.base_plate",
|
||||
)
|
||||
plate = scad.apply_tag(shape=plate, tag="demo.body.base_plate")
|
||||
|
||||
boss_profile = make_circle_profile(
|
||||
"center_boss",
|
||||
center_x,
|
||||
center_y,
|
||||
boss_r,
|
||||
"boss_outer",
|
||||
name="center_boss",
|
||||
center_x=center_x,
|
||||
center_y=center_y,
|
||||
radius=boss_r,
|
||||
circle_id="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")
|
||||
boss = scad.extrude_rsolid(
|
||||
profile=boss_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=boss_h + boss_overlap,
|
||||
tag_prefix="constrained_sketch.boss",
|
||||
result_tag="part.constrained_sketch.raised_boss",
|
||||
)
|
||||
boss = scad.translate_shape(
|
||||
shape=boss,
|
||||
vector=(0.0, 0.0, plate_t - boss_overlap),
|
||||
)
|
||||
boss = scad.apply_tag(shape=boss, tag="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",
|
||||
name="center_bore",
|
||||
center_x=center_x,
|
||||
center_y=center_y,
|
||||
radius=bore_r,
|
||||
circle_id="bore",
|
||||
)
|
||||
bore_cutter = scad.extrude_rsolid(
|
||||
bore_profile,
|
||||
(0.0, 0.0, 1.0),
|
||||
plate_t + boss_h + 2.0,
|
||||
profile=bore_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t + boss_h + 2.0,
|
||||
tag_prefix="constrained_sketch.bore.cutter",
|
||||
result_tag="tool.constrained_sketch.center_bore",
|
||||
)
|
||||
bore_cutter = scad.translate_shape(
|
||||
shape=bore_cutter,
|
||||
vector=(0.0, 0.0, -1.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,
|
||||
name="service_slot",
|
||||
x0=center_x - slot_w / 2.0,
|
||||
y0=slot_y - slot_h / 2.0,
|
||||
width=slot_w,
|
||||
height=slot_h,
|
||||
)
|
||||
slot_cutter = scad.extrude_rsolid(
|
||||
profile=slot_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t + 2.0,
|
||||
tag_prefix="constrained_sketch.slot.cutter",
|
||||
result_tag="tool.constrained_sketch.service_slot",
|
||||
)
|
||||
slot_cutter = scad.translate_shape(
|
||||
shape=slot_cutter,
|
||||
vector=(0.0, 0.0, -1.0),
|
||||
)
|
||||
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,
|
||||
name="guided_diamond_pocket",
|
||||
center_x=plate_w - 24.0,
|
||||
center_y=plate_h - 18.0,
|
||||
width=diamond_w,
|
||||
height=diamond_h,
|
||||
guide_gap=diamond_guide_gap,
|
||||
)
|
||||
diamond_cutter = scad.extrude_rsolid(
|
||||
profile=diamond_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t + 2.0,
|
||||
tag_prefix="constrained_sketch.diamond.cutter",
|
||||
result_tag="tool.constrained_sketch.diamond_pocket",
|
||||
)
|
||||
diamond_cutter = scad.translate_shape(
|
||||
shape=diamond_cutter,
|
||||
vector=(0.0, 0.0, -1.0),
|
||||
)
|
||||
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,
|
||||
name="curve_guided_relief",
|
||||
center_x=plate_w / 3.0,
|
||||
center_y=plate_h - 12.0,
|
||||
radius=relief_r,
|
||||
guide_span=relief_guide_span,
|
||||
)
|
||||
curve_relief_cutter = scad.extrude_rsolid(
|
||||
profile=curve_relief_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t + 2.0,
|
||||
tag_prefix="constrained_sketch.curve_relief.cutter",
|
||||
result_tag="tool.constrained_sketch.curve_relief",
|
||||
)
|
||||
curve_relief_cutter = scad.translate_shape(
|
||||
shape=curve_relief_cutter,
|
||||
vector=(0.0, 0.0, -1.0),
|
||||
)
|
||||
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),
|
||||
@@ -285,13 +569,27 @@ with scad.GraphSession() as session:
|
||||
]
|
||||
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_tag = name.replace("_", ".")
|
||||
mount_profile = make_circle_profile(
|
||||
name=name,
|
||||
center_x=x_pos,
|
||||
center_y=y_pos,
|
||||
radius=mount_r,
|
||||
circle_id="mount_hole",
|
||||
)
|
||||
mount_cutter = scad.extrude_rsolid(
|
||||
profile=mount_profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=plate_t + 2.0,
|
||||
tag_prefix=f"constrained_sketch.{mount_tag}.cutter",
|
||||
result_tag=f"tool.constrained_sketch.{mount_tag}",
|
||||
)
|
||||
mount_cutters.append(
|
||||
scad.translate_shape(
|
||||
shape=mount_cutter,
|
||||
vector=(0.0, 0.0, -1.0),
|
||||
)
|
||||
)
|
||||
mount_cutters.append(scad.translate_shape(mount_cutter, (0.0, 0.0, -1.0)))
|
||||
|
||||
part = scad.cut_rsolid(
|
||||
body,
|
||||
@@ -302,96 +600,126 @@ with scad.GraphSession() as session:
|
||||
mount_cutters,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
part = scad.apply_tag(part, "demo.constrained_sketch_bracket")
|
||||
part = scad.apply_tag(
|
||||
shape=part,
|
||||
tag="demo.constrained_sketch_bracket",
|
||||
)
|
||||
scad.capture_result(value=part)
|
||||
return {
|
||||
"part": part,
|
||||
"plate_profile": plate_profile,
|
||||
"diamond_profile": diamond_profile,
|
||||
"curve_relief_profile": curve_relief_profile,
|
||||
}
|
||||
|
||||
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))
|
||||
def main() -> None:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
result = build_model()
|
||||
data = result.value
|
||||
MODEL_JSON_PATH.write_text(result.model_json, encoding="utf-8")
|
||||
|
||||
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,
|
||||
)
|
||||
rebuilt = result.replay()
|
||||
scad.export_step(shapes=rebuilt, filename=str(STEP_PATH))
|
||||
|
||||
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.")
|
||||
)
|
||||
freecad_cmd = str(FREECAD_CMD) if FREECAD_CMD.exists() else None
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
json_str=result.model_json,
|
||||
output_path=str(FCSTD_PATH),
|
||||
document_name="SimpleCADConstrainedSketchDemo",
|
||||
freecad_cmd=freecad_cmd,
|
||||
)
|
||||
|
||||
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)
|
||||
payload = json.loads(result.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(items=data["plate_profile"].get_edges())
|
||||
.where(scad.ql.tag(pattern="sketch_entity.*"))
|
||||
.all()
|
||||
for tag in scad.list_tags(shape=edge)
|
||||
if tag.startswith("sketch_entity.")
|
||||
)
|
||||
diamond_entity_tags = sorted(
|
||||
tag
|
||||
for edge in scad.ql.select(items=data["diamond_profile"].get_edges())
|
||||
.where(scad.ql.tag(pattern="sketch_entity.*"))
|
||||
.all()
|
||||
for tag in scad.list_tags(shape=edge)
|
||||
if tag.startswith("sketch_entity.")
|
||||
)
|
||||
curve_entity_tags = sorted(
|
||||
tag
|
||||
for edge in scad.ql.select(items=data["curve_relief_profile"].get_edges())
|
||||
.where(scad.ql.tag(pattern="sketch_entity.*"))
|
||||
.all()
|
||||
for tag in scad.list_tags(shape=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(data["part"].get_volume(), 3))
|
||||
print("wrote", MODEL_JSON_PATH)
|
||||
print("wrote", STEP_PATH)
|
||||
print("wrote", FCSTD_PATH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -5,6 +5,7 @@ Run from the repository root with:
|
||||
|
||||
Generated files:
|
||||
examples/out/naca0016_blade/naca0016_blade.model.json
|
||||
examples/out/naca0016_blade/naca0016_blade.session.json
|
||||
examples/out/naca0016_blade/naca0016_blade.step
|
||||
examples/out/naca0016_blade/naca0016_blade.fcstd
|
||||
|
||||
@@ -27,26 +28,38 @@ 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:
|
||||
@scad.model(graph_id="naca0016_blade")
|
||||
def build_blade() -> scad.ModelResult:
|
||||
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(shape=blade, tag="role.naca0016.blade")
|
||||
blade = scad.apply_tag(shape=blade, tag="part.naca0016.blade")
|
||||
scad.capture_result(value=blade)
|
||||
return blade
|
||||
|
||||
|
||||
def write_blade_artifacts(
|
||||
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"
|
||||
session_json_path = output_dir / "naca0016_blade.session.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))
|
||||
result = build_blade()
|
||||
blade = result.value
|
||||
payload = json.loads(result.model_json)
|
||||
model_json_path.write_text(result.model_json, encoding="utf-8")
|
||||
session_json_path.write_text(result.session_json, encoding="utf-8")
|
||||
scad.export_step(shapes=blade, filename=str(step_path))
|
||||
|
||||
bspline_nodes = [
|
||||
node for node in payload["graph"]["nodes"] if node.get("op") == "make_spline_redge"
|
||||
@@ -63,12 +76,13 @@ def build_blade(output_dir: Path, *, freecad_cmd: Path | None = DEFAULT_FREECAD_
|
||||
print("bspline_knot_counts", knot_counts[:6])
|
||||
print("volume", round(blade.get_volume(), 6))
|
||||
print("wrote", model_json_path)
|
||||
print("wrote", session_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),
|
||||
json_str=result.model_json,
|
||||
output_path=str(fcstd_path),
|
||||
document_name="NACA0016Blade",
|
||||
freecad_cmd=str(freecad_cmd),
|
||||
)
|
||||
@@ -89,7 +103,7 @@ def main() -> None:
|
||||
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)
|
||||
write_blade_artifacts(args.output_dir, freecad_cmd=args.freecad_cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -9,53 +9,173 @@ import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
OUT_DIR = Path("examples/out/hydraulic_rod_assembly")
|
||||
OUT_DIR = Path(__file__).resolve().parent / "out" / "hydraulic_rod_assembly"
|
||||
|
||||
|
||||
@scad.model(graph_id="hydraulic_rod_assembly", export_dir=OUT_DIR)
|
||||
def build_hydraulic_rod_assembly():
|
||||
flange_holes = [
|
||||
(0.0, 16.0),
|
||||
(16.0, 0.0),
|
||||
(0.0, -16.0),
|
||||
(-16.0, 0.0),
|
||||
("zplus", 0.0, 16.0),
|
||||
("yplus", 16.0, 0.0),
|
||||
("zminus", 0.0, -16.0),
|
||||
("yminus", -16.0, 0.0),
|
||||
]
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
barrel = scad.make_cylinder_rsolid(
|
||||
@scad.requires_session
|
||||
def _build_in_session():
|
||||
def _named_cylinder(
|
||||
*,
|
||||
radius: float,
|
||||
height: float,
|
||||
bottom_face_center: tuple[float, float, float],
|
||||
axis: tuple[float, float, float],
|
||||
start_face_tag: str,
|
||||
end_face_tag: str,
|
||||
side_faces_tag: str,
|
||||
tag_prefix: str,
|
||||
result_tag: str,
|
||||
):
|
||||
return scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=bottom_face_center,
|
||||
axis=axis,
|
||||
tag_prefix=tag_prefix,
|
||||
result_tag=result_tag,
|
||||
start_face_tag=start_face_tag,
|
||||
end_face_tag=end_face_tag,
|
||||
side_face_tag=side_faces_tag,
|
||||
)
|
||||
|
||||
def _named_box(
|
||||
*,
|
||||
width: float,
|
||||
height: float,
|
||||
depth: float,
|
||||
bottom_face_center: tuple[float, float, float],
|
||||
bottom_face_tag: str,
|
||||
top_face_tag: str,
|
||||
side_faces_tag: str,
|
||||
tag_prefix: str,
|
||||
result_tag: str,
|
||||
):
|
||||
profile = scad.make_rectangle_rface(
|
||||
# Rectangle profile axes are Y/X for a +Z normal; swap the
|
||||
# dimensions to preserve make_box_rsolid's global X/Y layout.
|
||||
width=height,
|
||||
height=width,
|
||||
center=bottom_face_center,
|
||||
tag_prefix=f"{tag_prefix}.profile",
|
||||
edge_tags=("bottom", "right", "top", "left"),
|
||||
)
|
||||
return scad.extrude_rsolid(
|
||||
profile=profile,
|
||||
direction=(0.0, 0.0, 1.0),
|
||||
distance=depth,
|
||||
tag_prefix=tag_prefix,
|
||||
result_tag=result_tag,
|
||||
start_face_tag=bottom_face_tag,
|
||||
end_face_tag=top_face_tag,
|
||||
side_faces_tag=side_faces_tag,
|
||||
)
|
||||
|
||||
def _require_complete_face_naming(solid, prefix: str) -> None:
|
||||
unnamed = [
|
||||
index
|
||||
for index, face in enumerate(solid.get_faces())
|
||||
if not any(
|
||||
tag.startswith(prefix)
|
||||
for tag in scad.list_tags(face, scope="local")
|
||||
)
|
||||
]
|
||||
if unnamed:
|
||||
raise RuntimeError(
|
||||
f"{prefix} final face naming is incomplete at indices {unnamed}"
|
||||
)
|
||||
|
||||
def _require_shared_edge(solid, first_face_tag: str, second_face_tag: str):
|
||||
first = ql.faces().where(ql.tag(first_face_tag)).exactly(1)
|
||||
second = ql.faces().where(ql.tag(second_face_tag)).exactly(1)
|
||||
shared = first.shared_boundary(second).incident_face_count(exactly=2)
|
||||
incident = (
|
||||
ql.edges()
|
||||
.incident_to(first, second, distinct=True)
|
||||
.incident_face_count(exactly=2)
|
||||
)
|
||||
shared_edge = shared.exactly(1).resolve(solid)[0]
|
||||
incident_edge = incident.exactly(1).resolve(solid)[0]
|
||||
if shared_edge.topo_id != incident_edge.topo_id:
|
||||
raise RuntimeError(
|
||||
f"Face pair {first_face_tag!r}, {second_face_tag!r} "
|
||||
"did not resolve the same shared Edge"
|
||||
)
|
||||
return shared_edge
|
||||
|
||||
barrel = _named_cylinder(
|
||||
radius=16.0,
|
||||
height=120.0,
|
||||
bottom_face_center=(-60.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="sleeve.barrel.face.rear",
|
||||
end_face_tag="sleeve.barrel.face.front",
|
||||
side_faces_tag="sleeve.barrel.face.outer",
|
||||
tag_prefix="hydraulic.sleeve.barrel",
|
||||
result_tag="part.hydraulic.sleeve.barrel",
|
||||
)
|
||||
rod_gland_flange = scad.make_cylinder_rsolid(
|
||||
rod_gland_flange = _named_cylinder(
|
||||
radius=22.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(50.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="sleeve.gland.face.shoulder",
|
||||
end_face_tag="sleeve.gland.face.mount",
|
||||
side_faces_tag="sleeve.gland.face.outer",
|
||||
tag_prefix="hydraulic.sleeve.gland.flange",
|
||||
result_tag="part.hydraulic.sleeve.gland.flange",
|
||||
)
|
||||
rod_gland_nose = scad.make_cylinder_rsolid(
|
||||
rod_gland_nose = _named_cylinder(
|
||||
radius=13.0,
|
||||
height=10.0,
|
||||
bottom_face_center=(58.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="sleeve.gland.nose.face.rear",
|
||||
end_face_tag="sleeve.gland.nose.face.front",
|
||||
side_faces_tag="sleeve.gland.nose.face.outer",
|
||||
tag_prefix="hydraulic.sleeve.gland.nose",
|
||||
result_tag="part.hydraulic.sleeve.gland.nose",
|
||||
)
|
||||
base_cap = scad.make_cylinder_rsolid(
|
||||
base_cap = _named_cylinder(
|
||||
radius=18.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(-66.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="sleeve.base.cap.face.rear",
|
||||
end_face_tag="sleeve.base.cap.face.front",
|
||||
side_faces_tag="sleeve.base.cap.face.outer",
|
||||
tag_prefix="hydraulic.sleeve.base.cap",
|
||||
result_tag="part.hydraulic.sleeve.base.cap",
|
||||
)
|
||||
rear_eye = scad.make_cylinder_rsolid(
|
||||
rear_eye = _named_cylinder(
|
||||
radius=14.0,
|
||||
height=12.0,
|
||||
bottom_face_center=(-80.0, -6.0, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
start_face_tag="sleeve.rear.eye.face.ymin",
|
||||
end_face_tag="sleeve.rear.eye.face.ymax",
|
||||
side_faces_tag="sleeve.rear.eye.face.outer",
|
||||
tag_prefix="hydraulic.sleeve.rear.eye",
|
||||
result_tag="part.hydraulic.sleeve.rear.eye",
|
||||
)
|
||||
rear_eye_neck = scad.make_box_rsolid(
|
||||
18.0,
|
||||
14.0,
|
||||
16.0,
|
||||
rear_eye_neck = _named_box(
|
||||
width=18.0,
|
||||
height=14.0,
|
||||
depth=16.0,
|
||||
bottom_face_center=(-68.0, 0.0, -8.0),
|
||||
bottom_face_tag="sleeve.rear.neck.face.bottom",
|
||||
top_face_tag="sleeve.rear.neck.face.top",
|
||||
side_faces_tag="sleeve.rear.neck.face.side",
|
||||
tag_prefix="hydraulic.sleeve.rear.neck",
|
||||
result_tag="part.hydraulic.sleeve.rear.neck",
|
||||
)
|
||||
sleeve_raw = scad.union_rsolid(
|
||||
barrel,
|
||||
@@ -66,70 +186,112 @@ def build_hydraulic_rod_assembly():
|
||||
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),
|
||||
barrel_bore = _named_cylinder(
|
||||
radius=10.5,
|
||||
height=136.0,
|
||||
bottom_face_center=(-68.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="sleeve.barrel.bore.face.rear",
|
||||
end_face_tag="sleeve.barrel.bore.face.front",
|
||||
side_faces_tag="sleeve.barrel.bore.face.wall",
|
||||
tag_prefix="hydraulic.sleeve.barrel.bore",
|
||||
result_tag="tool.hydraulic.sleeve.barrel.bore",
|
||||
)
|
||||
sleeve_solid = scad.cut_rsolid(sleeve_raw, barrel_bore)
|
||||
rear_eye_pin_bore = _named_cylinder(
|
||||
radius=4.6,
|
||||
height=26.0,
|
||||
bottom_face_center=(-80.0, -13.0, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
start_face_tag="sleeve.rear.eye.pin.face.ymin",
|
||||
end_face_tag="sleeve.rear.eye.pin.face.ymax",
|
||||
side_faces_tag="sleeve.rear.eye.pin.face.wall",
|
||||
tag_prefix="hydraulic.sleeve.rear.eye.pin.bore",
|
||||
result_tag="tool.hydraulic.sleeve.rear.eye.pin_bore",
|
||||
)
|
||||
sleeve_solid = scad.cut_rsolid(sleeve_solid, rear_eye_pin_bore)
|
||||
for hole_id, y, z in flange_holes:
|
||||
bolt_hole = _named_cylinder(
|
||||
radius=1.8,
|
||||
height=16.0,
|
||||
bottom_face_center=(48.0, y, z),
|
||||
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:
|
||||
start_face_tag=f"sleeve.gland.bolt.{hole_id}.face.rear",
|
||||
end_face_tag=f"sleeve.gland.bolt.{hole_id}.face.front",
|
||||
side_faces_tag=f"sleeve.gland.bolt.{hole_id}.face.wall",
|
||||
tag_prefix=f"hydraulic.sleeve.gland.bolt.{hole_id}",
|
||||
result_tag=f"tool.hydraulic.sleeve.gland.bolt.{hole_id}",
|
||||
)
|
||||
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),
|
||||
),
|
||||
bolt_hole,
|
||||
)
|
||||
|
||||
piston_land_left = scad.make_cylinder_rsolid(
|
||||
piston_land_left = _named_cylinder(
|
||||
radius=10.0,
|
||||
height=3.2,
|
||||
bottom_face_center=(-6.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="rod.piston.land.left.face.rear",
|
||||
end_face_tag="rod.piston.land.left.face.front",
|
||||
side_faces_tag="rod.piston.land.left.face.outer",
|
||||
tag_prefix="hydraulic.rod.piston.land.left",
|
||||
result_tag="part.hydraulic.rod.piston.land.left",
|
||||
)
|
||||
piston_seal_groove = scad.make_cylinder_rsolid(
|
||||
piston_seal_groove = _named_cylinder(
|
||||
radius=9.0,
|
||||
height=6.0,
|
||||
bottom_face_center=(-3.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="rod.piston.groove.face.rear",
|
||||
end_face_tag="rod.piston.groove.face.front",
|
||||
side_faces_tag="rod.piston.groove.face.outer",
|
||||
tag_prefix="hydraulic.rod.piston.groove",
|
||||
result_tag="part.hydraulic.rod.piston.groove",
|
||||
)
|
||||
piston_land_right = scad.make_cylinder_rsolid(
|
||||
piston_land_right = _named_cylinder(
|
||||
radius=10.0,
|
||||
height=3.2,
|
||||
bottom_face_center=(2.6, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="rod.piston.land.right.face.rear",
|
||||
end_face_tag="rod.piston.land.right.face.front",
|
||||
side_faces_tag="rod.piston.land.right.face.outer",
|
||||
tag_prefix="hydraulic.rod.piston.land.right",
|
||||
result_tag="part.hydraulic.rod.piston.land.right",
|
||||
)
|
||||
chrome_rod = scad.make_cylinder_rsolid(
|
||||
chrome_rod = _named_cylinder(
|
||||
radius=6.5,
|
||||
height=132.0,
|
||||
bottom_face_center=(3.0, 0.0, 0.0),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
start_face_tag="rod.shaft.face.piston",
|
||||
end_face_tag="rod.shaft.face.eye",
|
||||
side_faces_tag="rod.shaft.face.outer",
|
||||
tag_prefix="hydraulic.rod.shaft",
|
||||
result_tag="part.hydraulic.rod.shaft",
|
||||
)
|
||||
rod_eye = scad.make_cylinder_rsolid(
|
||||
rod_eye = _named_cylinder(
|
||||
radius=13.0,
|
||||
height=9.0,
|
||||
bottom_face_center=(143.0, -4.5, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
start_face_tag="rod.eye.face.ymin",
|
||||
end_face_tag="rod.eye.face.ymax",
|
||||
side_faces_tag="rod.eye.face.outer",
|
||||
tag_prefix="hydraulic.rod.eye",
|
||||
result_tag="part.hydraulic.rod.eye",
|
||||
)
|
||||
rod_eye_neck = scad.make_box_rsolid(
|
||||
20.0,
|
||||
8.0,
|
||||
13.0,
|
||||
rod_eye_neck = _named_box(
|
||||
width=20.0,
|
||||
height=8.0,
|
||||
depth=13.0,
|
||||
bottom_face_center=(130.0, 0.0, -6.5),
|
||||
bottom_face_tag="rod.eye.neck.face.bottom",
|
||||
top_face_tag="rod.eye.neck.face.top",
|
||||
side_faces_tag="rod.eye.neck.face.side",
|
||||
tag_prefix="hydraulic.rod.eye.neck",
|
||||
result_tag="part.hydraulic.rod.eye.neck",
|
||||
)
|
||||
piston_rod_raw = scad.union_rsolid(
|
||||
piston_land_left,
|
||||
@@ -140,23 +302,56 @@ def build_hydraulic_rod_assembly():
|
||||
rod_eye_neck,
|
||||
glue=False,
|
||||
)
|
||||
rod_eye_pin_hole = scad.make_cylinder_rsolid(
|
||||
rod_eye_pin_hole = _named_cylinder(
|
||||
radius=5.5,
|
||||
height=13.0,
|
||||
bottom_face_center=(143.0, -6.5, 0.0),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
start_face_tag="rod.eye.pin.face.ymin",
|
||||
end_face_tag="rod.eye.pin.face.ymax",
|
||||
side_faces_tag="rod.eye.pin.face.wall",
|
||||
tag_prefix="hydraulic.rod.eye.pin.bore",
|
||||
result_tag="tool.hydraulic.rod.eye.pin_bore",
|
||||
)
|
||||
piston_rod_solid = scad.cut_rsolid(piston_rod_raw, rod_eye_pin_hole)
|
||||
|
||||
sleeve_solid = scad.apply_tag(
|
||||
shape=sleeve_solid,
|
||||
tag="part.hydraulic.sleeve.finished",
|
||||
)
|
||||
piston_rod_solid = scad.apply_tag(
|
||||
shape=piston_rod_solid,
|
||||
tag="part.hydraulic.rod.finished",
|
||||
)
|
||||
|
||||
_require_complete_face_naming(sleeve_solid, "sleeve.")
|
||||
_require_complete_face_naming(piston_rod_solid, "rod.")
|
||||
_require_shared_edge(
|
||||
sleeve_solid,
|
||||
"sleeve.gland.face.mount",
|
||||
"sleeve.gland.face.outer",
|
||||
)
|
||||
_require_shared_edge(
|
||||
sleeve_solid,
|
||||
"sleeve.gland.nose.face.front",
|
||||
"sleeve.barrel.bore.face.wall",
|
||||
)
|
||||
for hole_id in ("zplus", "yplus"):
|
||||
_require_shared_edge(
|
||||
sleeve_solid,
|
||||
"sleeve.gland.face.mount",
|
||||
f"sleeve.gland.bolt.{hole_id}.face.wall",
|
||||
)
|
||||
|
||||
black_oxide_steel = scad.make_material_rmaterial(
|
||||
"black_oxide_steel",
|
||||
material_id="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",
|
||||
material_id="chrome_plated_steel",
|
||||
name="Chrome plated steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
@@ -164,89 +359,117 @@ def build_hydraulic_rod_assembly():
|
||||
)
|
||||
|
||||
sleeve_part = scad.make_part_rpart(
|
||||
"outer_sleeve", sleeve_solid, name="Outer sleeve with clevis and gland"
|
||||
part_id="outer_sleeve",
|
||||
body=sleeve_solid,
|
||||
name="Outer sleeve with clevis and gland",
|
||||
)
|
||||
sleeve_part = scad.assign_material_rpart(
|
||||
part=sleeve_part, material=black_oxide_steel
|
||||
)
|
||||
sleeve_end_face = (
|
||||
ql.faces()
|
||||
.where(ql.tag("sleeve.gland.face.mount"))
|
||||
.exactly(1)
|
||||
.resolve(sleeve_solid)[0]
|
||||
)
|
||||
sleeve_connector = scad.make_face_connector_rconnector(
|
||||
connector_id="slide_axis", face=sleeve_end_face
|
||||
)
|
||||
sleeve_part = scad.add_connector_rpart(
|
||||
part=sleeve_part, connector=sleeve_connector
|
||||
)
|
||||
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"
|
||||
part_id="piston_rod",
|
||||
body=piston_rod_solid,
|
||||
name="Inner piston rod with eye end",
|
||||
)
|
||||
piston_rod_part = scad.assign_material_rpart(
|
||||
part=piston_rod_part, material=chrome_steel
|
||||
)
|
||||
rod_end_face = (
|
||||
ql.faces()
|
||||
.where(ql.tag("rod.piston.land.left.face.rear"))
|
||||
.exactly(1)
|
||||
.resolve(piston_rod_solid)[0]
|
||||
)
|
||||
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)
|
||||
rod_connector = scad.make_face_connector_rconnector(
|
||||
connector_id="slide_axis", face=rod_end_face, flip=rod_flip
|
||||
)
|
||||
piston_rod_part = scad.add_connector_rpart(
|
||||
part=piston_rod_part, connector=rod_connector
|
||||
)
|
||||
|
||||
hydraulic_assembly = scad.make_assembly_rassembly(
|
||||
"hydraulic_rod_assembly", name="Hydraulic rod assembly"
|
||||
assembly_id="hydraulic_rod_assembly", name="Hydraulic rod assembly"
|
||||
)
|
||||
hydraulic_assembly = scad.add_component_rassembly(
|
||||
hydraulic_assembly,
|
||||
sleeve_part,
|
||||
assembly=hydraulic_assembly,
|
||||
item=sleeve_part,
|
||||
component_id="outer_sleeve",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
hydraulic_assembly = scad.add_component_rassembly(
|
||||
hydraulic_assembly,
|
||||
piston_rod_part,
|
||||
assembly=hydraulic_assembly,
|
||||
item=piston_rod_part,
|
||||
component_id="inner_piston_rod",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
)
|
||||
hydraulic_assembly = scad.ground_component_rassembly(
|
||||
hydraulic_assembly, "outer_sleeve"
|
||||
assembly=hydraulic_assembly, component_id="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"),
|
||||
assembly=hydraulic_assembly,
|
||||
constraint_id="rod_slide",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="outer_sleeve", connector_id="slide_axis"
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="inner_piston_rod", connector_id="slide_axis"
|
||||
),
|
||||
drive_distance=0.0,
|
||||
distance_limit=scad.make_scalar_limit_rscalarlimit(0.0, 100.0),
|
||||
distance_limit=scad.make_scalar_limit_rscalarlimit(
|
||||
lower_value=0.0, upper_value=100.0
|
||||
),
|
||||
)
|
||||
hydraulic_assembly = scad.solve_assembly_constraints_rassembly(
|
||||
hydraulic_assembly
|
||||
assembly=hydraulic_assembly
|
||||
)
|
||||
|
||||
preview = scad.make_compound_from_assembly_rcompound(hydraulic_assembly)
|
||||
model_json = scad.export_model_json(session)
|
||||
preview = scad.make_compound_from_assembly_rcompound(
|
||||
assembly=hydraulic_assembly
|
||||
)
|
||||
preview = scad.apply_tag(
|
||||
shape=preview,
|
||||
tag="assembly.hydraulic.preview",
|
||||
)
|
||||
return hydraulic_assembly, preview
|
||||
|
||||
return hydraulic_assembly, preview, model_json
|
||||
hydraulic_assembly, preview = _build_in_session()
|
||||
scad.capture_result(value=(hydraulic_assembly, preview))
|
||||
return hydraulic_assembly, preview
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
assembly, preview, model_json = build_hydraulic_rod_assembly()
|
||||
result = build_hydraulic_rod_assembly()
|
||||
assembly, preview = result.value
|
||||
|
||||
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()))
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
json_str=result.model_json,
|
||||
output_path=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)
|
||||
payload = json.loads(result.model_json)
|
||||
face_count = len(ql.faces().resolve(preview))
|
||||
print("assembly", assembly.assembly_id)
|
||||
print("components", assembly.component_ids())
|
||||
@@ -254,8 +477,8 @@ def main() -> None:
|
||||
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)
|
||||
for path in result.artifact_paths.values():
|
||||
print("wrote", path)
|
||||
print("fcstd", fcstd_status)
|
||||
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,636 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,847 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,168 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,101 +0,0 @@
|
||||
"""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()
|
||||
@@ -4,37 +4,68 @@ 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
|
||||
|
||||
if __package__:
|
||||
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
|
||||
else:
|
||||
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
|
||||
|
||||
@scad.requires_session
|
||||
def make_two_stage_planetary_reducer_rassembly() -> scad.Assembly:
|
||||
"""Build the full 20:1 compact reducer assembly and solve constraints."""
|
||||
|
||||
@@ -84,6 +115,7 @@ def make_two_stage_planetary_reducer_rassembly() -> scad.Assembly:
|
||||
bearing = make_radial_ball_bearing_rassembly(
|
||||
bearing_id="micro_radial_ball_bearing",
|
||||
spec=UNIVERSAL_RADIAL_BEARING,
|
||||
tag_prefix="reducer.bearing.micro.radial",
|
||||
)
|
||||
|
||||
reducer = scad.make_assembly_rassembly(
|
||||
@@ -133,6 +165,7 @@ def make_two_stage_planetary_reducer_rassembly() -> scad.Assembly:
|
||||
return reducer
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_fixed_components_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
@@ -150,6 +183,7 @@ def _add_fixed_components_rassembly(
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_bearing_components_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
@@ -211,6 +245,7 @@ def _add_bearing_components_rassembly(
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_public_interface_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
"""Expose stable actuator module datums without leaking private component ids."""
|
||||
|
||||
@@ -231,6 +266,7 @@ def _add_public_interface_connectors_rassembly(*, assembly: scad.Assembly) -> sc
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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")
|
||||
@@ -290,6 +326,7 @@ def _add_reducer_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assem
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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"),
|
||||
@@ -338,6 +375,7 @@ def _add_bearing_interface_constraints_rassembly(*, assembly: scad.Assembly) ->
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_stage_mesh_constraints_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
@@ -388,6 +426,7 @@ def _add_stage_mesh_constraints_rassembly(
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _gear_stage_rplacement(*, stage: StageSpec) -> scad.Placement:
|
||||
return scad.make_placement_rplacement(
|
||||
origin=(0.0, 0.0, stage.bottom_z),
|
||||
@@ -396,6 +435,7 @@ def _gear_stage_rplacement(*, stage: StageSpec) -> scad.Placement:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
return scad.make_connector_ref_rconnectorref(
|
||||
component_id=component_id,
|
||||
|
||||
@@ -10,10 +10,12 @@ from common import make_z_rotation_rplacement
|
||||
from dimensions import BearingSpec, PLANET_COUNT, StageSpec
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_radial_ball_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
spec: BearingSpec,
|
||||
tag_prefix: str,
|
||||
) -> scad.Assembly:
|
||||
"""Create a standard radial ball bearing assembly for reducer placement."""
|
||||
|
||||
@@ -30,7 +32,11 @@ def make_radial_ball_bearing_rassembly(
|
||||
)
|
||||
meta = bearing.get_metadata("std.bearing.ball_bearing")
|
||||
outer_ring = bearing.get_component("outer_ring").item.body
|
||||
outer_ring = scad.apply_tag(shape=outer_ring, tag=f"solid.{tag_prefix}.outer.ring")
|
||||
inner_ring = bearing.get_component("inner_ring").item.body
|
||||
inner_ring = scad.apply_tag(shape=inner_ring, tag=f"solid.{tag_prefix}.inner.ring")
|
||||
rolling_element = bearing.get_component(meta["ball_component_ids"][0]).item.body
|
||||
scad.apply_tag(shape=rolling_element, tag=f"solid.{tag_prefix}.rolling.element")
|
||||
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}"
|
||||
@@ -42,12 +48,14 @@ def make_radial_ball_bearing_rassembly(
|
||||
return bearing
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_planet_bearing_rplacements(*, stage: StageSpec) -> list[scad.Placement]:
|
||||
"""Return placed bearing placements centered in all planets of one stage."""
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from dimensions import (
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_carrier_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -55,6 +56,7 @@ def make_stage_carrier_rpart(
|
||||
pad_radius=STAGE1_PAD_RADIUS,
|
||||
central_shaft_radius=STAGE1_CARRIER_SHAFT_RADIUS,
|
||||
central_shaft_top_z=STAGE_2.top_z,
|
||||
tag_prefix="reducer.stage1.carrier",
|
||||
)
|
||||
connector_specs = [
|
||||
{
|
||||
@@ -83,6 +85,7 @@ def make_stage_carrier_rpart(
|
||||
pad_radius=STAGE2_PAD_RADIUS,
|
||||
central_shaft_radius=OUTPUT_SHAFT_RADIUS,
|
||||
central_shaft_top_z=OUTPUT_FLANGE_TOP_Z,
|
||||
tag_prefix="reducer.stage2.carrier",
|
||||
)
|
||||
connector_specs = [
|
||||
{
|
||||
@@ -142,6 +145,7 @@ def make_stage_carrier_rpart(
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _make_carrier_solid_rsolid(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -155,12 +159,15 @@ def _make_carrier_solid_rsolid(
|
||||
pad_radius: float,
|
||||
central_shaft_radius: float,
|
||||
central_shaft_top_z: float,
|
||||
tag_prefix: str,
|
||||
) -> 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),
|
||||
tag_prefix=f"{tag_prefix}.hub",
|
||||
result_tag=f"solid.{tag_prefix}.hub",
|
||||
)
|
||||
solids = [hub]
|
||||
|
||||
@@ -171,6 +178,8 @@ def _make_carrier_solid_rsolid(
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.shaft",
|
||||
result_tag=f"solid.{tag_prefix}.shaft",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -195,6 +204,8 @@ def _make_carrier_solid_rsolid(
|
||||
height=arm_width,
|
||||
depth=plate_thickness,
|
||||
bottom_face_center=(arm_center_radius, 0.0, plate_bottom_z),
|
||||
tag_prefix=f"{tag_prefix}.arm.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.arm.i{index + 1}",
|
||||
)
|
||||
if abs(carrier_angle) > 1.0e-9:
|
||||
arm = scad.rotate_shape(
|
||||
@@ -210,6 +221,8 @@ def _make_carrier_solid_rsolid(
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.pad.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.pad.i{index + 1}",
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
@@ -218,6 +231,8 @@ def _make_carrier_solid_rsolid(
|
||||
height=pin_height,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.pin.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.pin.i{index + 1}",
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
@@ -226,6 +241,8 @@ def _make_carrier_solid_rsolid(
|
||||
height=pin_land_height,
|
||||
bottom_face_center=(center_xy[0], center_xy[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.pin.land.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.pin.land.i{index + 1}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ import time
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from assembly import make_two_stage_planetary_reducer_rassembly
|
||||
if __package__:
|
||||
from .main import _build_compact_two_stage_planetary_reducer
|
||||
else:
|
||||
from main import _build_compact_two_stage_planetary_reducer
|
||||
|
||||
|
||||
sys.setrecursionlimit(30000)
|
||||
@@ -28,8 +31,8 @@ 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()
|
||||
result = _build_compact_two_stage_planetary_reducer()
|
||||
assembly, _preview = result.value
|
||||
elapsed = time.perf_counter() - start
|
||||
return assembly, len(log_buffer.getvalue().splitlines()), elapsed
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_z_rotation_rplacement(
|
||||
*,
|
||||
origin: tuple[float, float, float],
|
||||
@@ -26,12 +27,14 @@ def make_z_rotation_rplacement(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_annular_cylinder_rsolid(
|
||||
*,
|
||||
outer_radius: float,
|
||||
inner_radius: float,
|
||||
height: float,
|
||||
bottom_z: float,
|
||||
tag_prefix: str,
|
||||
tag: str,
|
||||
) -> scad.Solid:
|
||||
"""Create a single hollow cylindrical solid with a through bore."""
|
||||
@@ -43,12 +46,16 @@ def make_annular_cylinder_rsolid(
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.outer",
|
||||
result_tag=f"solid.{tag_prefix}.outer",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.bore",
|
||||
result_tag=f"solid.{tag_prefix}.bore.cutter",
|
||||
)
|
||||
annular = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
annular = scad.apply_tag(shape=annular, tag=tag)
|
||||
@@ -56,6 +63,7 @@ def make_annular_cylinder_rsolid(
|
||||
return annular
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_axis_connector_rconnector(
|
||||
*,
|
||||
connector_id: str,
|
||||
@@ -83,6 +91,7 @@ def make_axis_connector_rconnector(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_axis_part_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
@@ -113,6 +122,7 @@ def make_axis_part_rpart(
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def add_placement_axis_connector_rpart(
|
||||
*,
|
||||
part: scad.Part,
|
||||
@@ -130,6 +140,7 @@ def add_placement_axis_connector_rpart(
|
||||
return scad.add_connector_rpart(part=part, connector=connector)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _apply_tags(shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
|
||||
"""Apply normalized tags through the public SimpleCAD tag API."""
|
||||
|
||||
@@ -172,9 +183,13 @@ def _axis_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()
|
||||
local_roles = [
|
||||
tag
|
||||
for tag in scad.list_tags(shape=solid, scope="local")
|
||||
if tag.startswith("role.")
|
||||
]
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
|
||||
f"{label}: faces={len(faces)} local_roles={len(local_roles)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ from dimensions import (
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_input_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the reducer input flange part with six bolt holes."""
|
||||
|
||||
@@ -61,6 +62,7 @@ def make_input_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_count=INPUT_FLANGE_HOLE_COUNT,
|
||||
counterbore_diameter=INPUT_FLANGE_HOLE_COUNTERBORE_DIAMETER,
|
||||
counterbore_depth=INPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
tag_prefix="reducer.input.flange",
|
||||
)
|
||||
flange = scad.translate_shape(
|
||||
shape=flange,
|
||||
@@ -90,10 +92,11 @@ def make_input_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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 = _make_output_flange_solid_rsolid(tag_prefix="reducer.output.flange")
|
||||
flange = scad.translate_shape(
|
||||
shape=flange,
|
||||
vector=(0.0, 0.0, OUTPUT_FLANGE_BOTTOM_Z),
|
||||
@@ -123,7 +126,8 @@ def make_output_flange_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
@scad.requires_session
|
||||
def _make_output_flange_solid_rsolid(*, tag_prefix: str) -> scad.Solid:
|
||||
"""Build the sealed actuator-style output flange.
|
||||
|
||||
The earlier example used a small six-hole disk. That was enough to prove
|
||||
@@ -139,12 +143,16 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
height=OUTPUT_FLANGE_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.base",
|
||||
result_tag=f"solid.{tag_prefix}.base",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.boss",
|
||||
result_tag=f"solid.{tag_prefix}.boss",
|
||||
)
|
||||
|
||||
register_outer = scad.make_cylinder_rsolid(
|
||||
@@ -152,12 +160,16 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.register.outer",
|
||||
result_tag=f"solid.{tag_prefix}.register.outer",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.register.inner",
|
||||
result_tag=f"solid.{tag_prefix}.register.inner.cutter",
|
||||
)
|
||||
register = scad.cut_rsolid(register_outer, register_inner, skip_non_intersecting=False)
|
||||
|
||||
@@ -173,6 +185,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.center.bore",
|
||||
result_tag=f"solid.{tag_prefix}.center.bore.cutter",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -193,6 +207,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
0.0,
|
||||
OUTPUT_FLANGE_THICKNESS - 0.25,
|
||||
),
|
||||
tag_prefix=f"{tag_prefix}.register.gap.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.register.gap.i{index + 1}.cutter",
|
||||
)
|
||||
cutters.append(
|
||||
scad.rotate_shape(
|
||||
@@ -211,7 +227,7 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
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:
|
||||
for index, angle_degrees in enumerate(output_hole_angles):
|
||||
angle = math.radians(angle_degrees)
|
||||
x = output_bolt_radius * math.cos(angle)
|
||||
y = output_bolt_radius * math.sin(angle)
|
||||
@@ -225,6 +241,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
height=OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_REGISTER_HEIGHT + 1.0,
|
||||
bottom_face_center=(x, y, -0.5),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.link.hole.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.link.hole.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
@@ -239,6 +257,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
- OUTPUT_FLANGE_HOLE_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.link.counterbore.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.link.counterbore.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -257,6 +277,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
height=OUTPUT_FLANGE_THICKNESS + OUTPUT_FLANGE_BOSS_HEIGHT + 1.0,
|
||||
bottom_face_center=(x, y, -0.5),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.cap.hole.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.cap.hole.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
@@ -271,6 +293,8 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
- OUTPUT_FLANGE_CENTER_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.cap.counterbore.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.cap.counterbore.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -288,6 +312,7 @@ def _make_output_flange_solid_rsolid() -> scad.Solid:
|
||||
return flange
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _make_n_hole_flange_solid_rsolid(
|
||||
*,
|
||||
flange_outer_diameter: float,
|
||||
@@ -300,6 +325,7 @@ def _make_n_hole_flange_solid_rsolid(
|
||||
hole_count: int,
|
||||
counterbore_diameter: float | None = None,
|
||||
counterbore_depth: float = 0.0,
|
||||
tag_prefix: str,
|
||||
) -> scad.Solid:
|
||||
"""Build a flange without edge-pick features so FreeCAD export is stable."""
|
||||
|
||||
@@ -308,12 +334,16 @@ def _make_n_hole_flange_solid_rsolid(
|
||||
height=flange_thickness,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.base",
|
||||
result_tag=f"solid.{tag_prefix}.base",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.boss",
|
||||
result_tag=f"solid.{tag_prefix}.boss",
|
||||
)
|
||||
flange = scad.union_rsolid([outer, boss], glue=False)
|
||||
|
||||
@@ -323,6 +353,8 @@ def _make_n_hole_flange_solid_rsolid(
|
||||
height=flange_thickness + boss_height + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.center.bore",
|
||||
result_tag=f"solid.{tag_prefix}.center.bore.cutter",
|
||||
)
|
||||
]
|
||||
bolt_circle_radius = hole_circle_diameter / 2.0
|
||||
@@ -338,6 +370,8 @@ def _make_n_hole_flange_solid_rsolid(
|
||||
-1.0,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.mount.hole.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.mount.hole.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
if counterbore_diameter is not None and counterbore_depth > 0.0:
|
||||
@@ -354,6 +388,8 @@ def _make_n_hole_flange_solid_rsolid(
|
||||
flange_thickness - counterbore_depth,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.mount.counterbore.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.mount.counterbore.i{index + 1}.cutter",
|
||||
)
|
||||
)
|
||||
flange = scad.cut_rsolid(flange, cutters, skip_non_intersecting=False)
|
||||
|
||||
@@ -28,6 +28,7 @@ from dimensions import (
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_ring_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -46,25 +47,35 @@ def make_stage_ring_gear_rpart(
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
)
|
||||
ring = scad.apply_tag(shape=ring, tag=f"solid.reducer.{stage.stage_id}.ring.gear")
|
||||
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),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.ring.support.outer",
|
||||
result_tag=f"solid.reducer.{stage.stage_id}.ring.support.outer",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.ring.support.bore",
|
||||
result_tag=f"solid.reducer.{stage.stage_id}.ring.support.bore.cutter",
|
||||
)
|
||||
support = scad.cut_rsolid(
|
||||
support,
|
||||
support_bore,
|
||||
skip_non_intersecting=False,
|
||||
tracking_policy=scad.TrackingPolicy.GRAPH,
|
||||
)
|
||||
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 = scad.union_rsolid(
|
||||
[ring, support],
|
||||
glue=False,
|
||||
tracking_policy=scad.TrackingPolicy.GRAPH,
|
||||
)
|
||||
ring = _apply_tags(
|
||||
ring,
|
||||
tags=(f"role.{stage.stage_id}.fixed_ring_gear", "group.two_stage_reducer"),
|
||||
@@ -91,6 +102,7 @@ def make_stage_ring_gear_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_sun_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -109,10 +121,12 @@ def make_stage_sun_gear_rpart(
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
sun = scad.apply_tag(shape=sun, tag=f"solid.reducer.{stage.stage_id}.sun.gear")
|
||||
sun = _cut_bore_rsolid(
|
||||
label=f"{stage.stage_id}_sun_bore",
|
||||
solid=sun,
|
||||
bore_radius=bore_radius,
|
||||
tag_prefix=f"reducer.{stage.stage_id}.sun.bore",
|
||||
)
|
||||
sun = _apply_tags(
|
||||
sun,
|
||||
@@ -139,6 +153,7 @@ def make_stage_sun_gear_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_planet_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -157,11 +172,13 @@ def make_stage_planet_gear_rpart(
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
planet = scad.apply_tag(shape=planet, tag=f"solid.reducer.{stage.stage_id}.planet.gear")
|
||||
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,
|
||||
tag_prefix=f"reducer.{stage.stage_id}.planet.bearing.seat",
|
||||
)
|
||||
planet = _apply_tags(
|
||||
planet,
|
||||
@@ -194,6 +211,7 @@ def make_stage_planet_gear_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_planet_component_rplacement(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -216,19 +234,28 @@ def make_planet_component_rplacement(
|
||||
return make_z_rotation_rplacement(origin=center, angle_degrees=planet_spin)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _cut_bore_rsolid(
|
||||
*,
|
||||
label: str,
|
||||
solid: scad.Solid,
|
||||
bore_radius: float,
|
||||
tag_prefix: str,
|
||||
) -> 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),
|
||||
tag_prefix=tag_prefix,
|
||||
result_tag=f"solid.{tag_prefix}.cutter",
|
||||
)
|
||||
bored = scad.cut_rsolid(
|
||||
solid,
|
||||
cutter,
|
||||
skip_non_intersecting=False,
|
||||
tracking_policy=scad.TrackingPolicy.GRAPH,
|
||||
)
|
||||
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
|
||||
|
||||
@@ -48,6 +48,7 @@ from dimensions import (
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the through-bolted housing sleeve with internal datum collars."""
|
||||
|
||||
@@ -56,6 +57,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=HOUSING_INNER_RADIUS,
|
||||
height=HOUSING_HEIGHT,
|
||||
bottom_z=HOUSING_BOTTOM_Z,
|
||||
tag_prefix="reducer.housing.sleeve",
|
||||
tag="role.housing_sleeve",
|
||||
)
|
||||
front_flange = _make_end_flange_rsolid(
|
||||
@@ -63,14 +65,16 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=OUTPUT_SEAL_BORE_RADIUS,
|
||||
thickness=HOUSING_FRONT_FLANGE_THICKNESS,
|
||||
bottom_z=HOUSING_BOTTOM_Z + HOUSING_HEIGHT - HOUSING_FRONT_FLANGE_THICKNESS,
|
||||
tag_prefix="reducer.housing.front.end.cap",
|
||||
)
|
||||
rear_flange = _make_end_flange_rsolid(
|
||||
label="rear",
|
||||
inner_radius=INPUT_SEAL_BORE_RADIUS,
|
||||
thickness=HOUSING_REAR_FLANGE_THICKNESS,
|
||||
bottom_z=HOUSING_BOTTOM_Z,
|
||||
tag_prefix="reducer.housing.rear.end.cap",
|
||||
)
|
||||
mount_pad = _make_mount_sector_pad_rsolid()
|
||||
mount_pad = _make_mount_sector_pad_rsolid(tag_prefix="reducer.housing.mount.sector.pad")
|
||||
collars = []
|
||||
datum_zs = (
|
||||
INPUT_FLANGE_TOP_Z,
|
||||
@@ -91,6 +95,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=HOUSING_DATUM_INNER_RADIUS,
|
||||
height=0.36,
|
||||
bottom_z=target_z - 0.36,
|
||||
tag_prefix=f"reducer.housing.datum.collar.i{index + 1}",
|
||||
tag=f"role.housing_axis_datum_{index + 1}",
|
||||
)
|
||||
collars.append(collar)
|
||||
@@ -102,8 +107,8 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
housing = scad.cut_rsolid(
|
||||
housing,
|
||||
[
|
||||
_make_mount_gap_cutters_rsolids(),
|
||||
_make_mount_hole_cutters_rsolids(),
|
||||
_make_mount_gap_cutters_rsolids(tag_prefix="reducer.housing.mount.gap"),
|
||||
_make_mount_hole_cutters_rsolids(tag_prefix="reducer.housing.mount.hole"),
|
||||
],
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
@@ -155,12 +160,14 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _make_end_flange_rsolid(
|
||||
*,
|
||||
label: str,
|
||||
inner_radius: float,
|
||||
thickness: float,
|
||||
bottom_z: float,
|
||||
tag_prefix: str,
|
||||
) -> scad.Solid:
|
||||
"""Build one sealed housing end cap.
|
||||
|
||||
@@ -174,6 +181,7 @@ def _make_end_flange_rsolid(
|
||||
inner_radius=inner_radius,
|
||||
height=thickness,
|
||||
bottom_z=bottom_z,
|
||||
tag_prefix=tag_prefix,
|
||||
tag=f"role.housing_{label}_sealed_end_cap",
|
||||
)
|
||||
clearance = OUTPUT_SEAL_RUNNING_CLEARANCE if label == "front" else INPUT_SEAL_RUNNING_CLEARANCE
|
||||
@@ -183,7 +191,8 @@ def _make_end_flange_rsolid(
|
||||
return flange
|
||||
|
||||
|
||||
def _make_mount_sector_pad_rsolid() -> scad.Solid:
|
||||
@scad.requires_session
|
||||
def _make_mount_sector_pad_rsolid(*, tag_prefix: str) -> scad.Solid:
|
||||
"""Build four graceful full-height sector pads before the global hole cut."""
|
||||
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
@@ -191,12 +200,16 @@ def _make_mount_sector_pad_rsolid() -> scad.Solid:
|
||||
height=HOUSING_HEIGHT,
|
||||
bottom_face_center=(0.0, 0.0, HOUSING_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.outer",
|
||||
result_tag=f"solid.{tag_prefix}.outer",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.inner",
|
||||
result_tag=f"solid.{tag_prefix}.inner.cutter",
|
||||
)
|
||||
pad = scad.cut_rsolid(outer, inner, skip_non_intersecting=False)
|
||||
pad = _apply_tags(
|
||||
@@ -211,7 +224,8 @@ def _make_mount_sector_pad_rsolid() -> scad.Solid:
|
||||
return pad
|
||||
|
||||
|
||||
def _make_mount_gap_cutters_rsolids() -> list[scad.Solid]:
|
||||
@scad.requires_session
|
||||
def _make_mount_gap_cutters_rsolids(*, tag_prefix: str) -> list[scad.Solid]:
|
||||
"""Build shallow radial gap cutters that divide the outer band into sectors."""
|
||||
|
||||
cutters = []
|
||||
@@ -229,6 +243,8 @@ def _make_mount_gap_cutters_rsolids() -> list[scad.Solid]:
|
||||
height=HOUSING_MOUNT_SECTOR_GAP_WIDTH,
|
||||
depth=HOUSING_HEIGHT + 2.0,
|
||||
bottom_face_center=(gap_center_radius, 0.0, HOUSING_BOTTOM_Z - 1.0),
|
||||
tag_prefix=f"{tag_prefix}.i{index + 1}",
|
||||
result_tag=f"solid.{tag_prefix}.i{index + 1}.cutter",
|
||||
)
|
||||
cutters.append(
|
||||
scad.rotate_shape(
|
||||
@@ -245,7 +261,8 @@ def _make_mount_gap_cutters_rsolids() -> list[scad.Solid]:
|
||||
return cutters
|
||||
|
||||
|
||||
def _make_mount_hole_cutters_rsolids() -> list[scad.Solid]:
|
||||
@scad.requires_session
|
||||
def _make_mount_hole_cutters_rsolids(*, tag_prefix: str) -> list[scad.Solid]:
|
||||
"""Build one shared cutter set for the boss and housing body holes."""
|
||||
|
||||
cutters = []
|
||||
@@ -262,11 +279,23 @@ def _make_mount_hole_cutters_rsolids() -> list[scad.Solid]:
|
||||
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))
|
||||
cutters.extend(
|
||||
_make_single_mount_hole_cutters_rsolids(
|
||||
angle=angle,
|
||||
tag_prefix=(
|
||||
f"{tag_prefix}.sector.i{sector_index + 1}.i{hole_index + 1}"
|
||||
),
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
|
||||
|
||||
def _make_single_mount_hole_cutters_rsolids(*, angle: float) -> list[scad.Solid]:
|
||||
@scad.requires_session
|
||||
def _make_single_mount_hole_cutters_rsolids(
|
||||
*,
|
||||
angle: float,
|
||||
tag_prefix: str,
|
||||
) -> list[scad.Solid]:
|
||||
"""Build through and counterbore cutters for one housing screw."""
|
||||
|
||||
x = HOUSING_MOUNT_HOLE_CIRCLE_RADIUS * math.cos(angle)
|
||||
@@ -282,6 +311,8 @@ def _make_single_mount_hole_cutters_rsolids(*, angle: float) -> list[scad.Solid]
|
||||
height=HOUSING_HEIGHT + 2.0,
|
||||
bottom_face_center=(x, y, HOUSING_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.through",
|
||||
result_tag=f"solid.{tag_prefix}.through.cutter",
|
||||
)
|
||||
)
|
||||
# Counterbores are added on both ends so the actuator can be mounted from
|
||||
@@ -297,6 +328,8 @@ def _make_single_mount_hole_cutters_rsolids(*, angle: float) -> list[scad.Solid]
|
||||
HOUSING_BOTTOM_Z + HOUSING_HEIGHT - HOUSING_MOUNT_COUNTERBORE_DEPTH,
|
||||
),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.front.counterbore",
|
||||
result_tag=f"solid.{tag_prefix}.front.counterbore.cutter",
|
||||
)
|
||||
)
|
||||
cutters.append(
|
||||
@@ -305,6 +338,8 @@ def _make_single_mount_hole_cutters_rsolids(*, angle: float) -> list[scad.Solid]
|
||||
height=HOUSING_MOUNT_COUNTERBORE_DEPTH + 0.4,
|
||||
bottom_face_center=(x, y, HOUSING_BOTTOM_Z - 0.2),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.rear.counterbore",
|
||||
result_tag=f"solid.{tag_prefix}.rear.counterbore.cutter",
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
|
||||
@@ -8,9 +8,34 @@ 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
|
||||
if __package__:
|
||||
import importlib
|
||||
|
||||
# The example also supports direct-file execution, so its child modules use
|
||||
# sibling imports. Register those siblings before importing the package entry.
|
||||
for _module_name in (
|
||||
"dimensions",
|
||||
"common",
|
||||
"materials",
|
||||
"bearings",
|
||||
"carriers",
|
||||
"flanges",
|
||||
"gears",
|
||||
"housing",
|
||||
"shafts",
|
||||
"assembly",
|
||||
):
|
||||
sys.modules.setdefault(
|
||||
_module_name,
|
||||
importlib.import_module(f"{__package__}.{_module_name}"),
|
||||
)
|
||||
from .assembly import make_two_stage_planetary_reducer_rassembly
|
||||
from .common import _ground_compound
|
||||
from .dimensions import HOUSING_HEIGHT, HOUSING_OUTER_RADIUS, TOTAL_REDUCTION
|
||||
else:
|
||||
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.
|
||||
@@ -19,16 +44,16 @@ sys.setrecursionlimit(30000)
|
||||
OUT_DIR = Path("examples/out/compact_two_stage_planetary_reducer")
|
||||
|
||||
|
||||
@scad.model(graph_id="compact_two_stage_planetary_reducer")
|
||||
def _build_compact_two_stage_planetary_reducer():
|
||||
"""Build the reducer and return assembly, preview compound, and JSON exports."""
|
||||
"""Build the reducer and return its assembly and preview compound."""
|
||||
|
||||
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
|
||||
assembly = make_two_stage_planetary_reducer_rassembly()
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
preview = scad.apply_tag(shape=preview, tag="scene.reducer.preview")
|
||||
_ground_compound(label="reducer_preview", compound=preview)
|
||||
scad.capture_result(value=(assembly, preview))
|
||||
return assembly, preview
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -42,19 +67,20 @@ def main() -> None:
|
||||
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")
|
||||
result = _build_compact_two_stage_planetary_reducer()
|
||||
assembly, preview = result.value
|
||||
model_path.write_text(result.model_json, encoding="utf-8")
|
||||
session_path.write_text(result.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)
|
||||
imported = scad.import_model_json(json_str=result.model_json)
|
||||
replayed = scad.replay_model_json(json_str=result.model_json)
|
||||
payload = json.loads(result.model_json)
|
||||
|
||||
fcstd_status = "not attempted"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
json_str=model_json,
|
||||
json_str=result.model_json,
|
||||
output_path=str(fcstd_path.resolve()),
|
||||
document_name="CompactTwoStagePlanetaryReducer",
|
||||
freecad_cmd=None,
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_reducer_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create the small set of reusable material records for the assembly."""
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from common import _apply_tags, add_placement_axis_connector_rpart, make_axis_pa
|
||||
from dimensions import INPUT_BEARING_Z, INPUT_FLANGE_TOP_Z, INPUT_SHAFT_RADIUS, STAGE_1
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_input_shaft_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the input shaft linking the input flange and stage 1 sun."""
|
||||
|
||||
@@ -17,6 +18,8 @@ def make_input_shaft_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, INPUT_FLANGE_TOP_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix="reducer.input.shaft",
|
||||
result_tag="solid.reducer.input.shaft",
|
||||
)
|
||||
shaft = _apply_tags(
|
||||
shaft,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,142 +0,0 @@
|
||||
"""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
|
||||
@@ -1,194 +0,0 @@
|
||||
"""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",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1,151 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,164 +0,0 @@
|
||||
"""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"),),
|
||||
)
|
||||
@@ -1,254 +0,0 @@
|
||||
"""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}"
|
||||
)
|
||||
@@ -1,343 +0,0 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -1,173 +0,0 @@
|
||||
"""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}"
|
||||
)
|
||||
@@ -1,61 +0,0 @@
|
||||
"""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),
|
||||
),
|
||||
}
|
||||
@@ -1,592 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,90 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,163 +0,0 @@
|
||||
"""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}"
|
||||
)
|
||||
@@ -1,78 +0,0 @@
|
||||
"""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))}"
|
||||
)
|
||||
@@ -1,54 +0,0 @@
|
||||
"""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),
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,35 +0,0 @@
|
||||
"""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
|
||||
@@ -1,206 +0,0 @@
|
||||
"""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)
|
||||
@@ -88,6 +88,7 @@ except ImportError: # Support direct execution from this example directory.
|
||||
from motor import make_bldc_rotor_rassembly, make_bldc_stator_rassembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_integrated_bldc_joint_actuator_rassembly(
|
||||
*, materials: dict[str, scad.Material]
|
||||
) -> scad.Assembly:
|
||||
@@ -116,6 +117,7 @@ def make_integrated_bldc_joint_actuator_rassembly(
|
||||
return actuator
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_integrated_bldc_joint_actuator_components_rtuple(
|
||||
*, materials: dict[str, scad.Material]
|
||||
) -> tuple[tuple[str, scad.Part | scad.Assembly, scad.Placement, str], ...]:
|
||||
@@ -258,6 +260,7 @@ def make_integrated_bldc_joint_actuator_components_rtuple(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
forwarded = (
|
||||
("case_clamp_axis", "reducer_housing", "case_clamp_axis", "External split-clamp datum"),
|
||||
@@ -279,6 +282,7 @@ def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembl
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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")
|
||||
@@ -339,6 +343,7 @@ def _add_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _add_stage_constraints_rassembly(
|
||||
*,
|
||||
assembly: scad.Assembly,
|
||||
@@ -386,6 +391,7 @@ def _add_stage_constraints_rassembly(
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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"),
|
||||
@@ -437,5 +443,6 @@ def _add_bearing_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assem
|
||||
return assembly
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _stage_rplacement(*, stage: StageSpec) -> scad.Placement:
|
||||
return scad.make_placement_rplacement(origin=(0.0, 0.0, stage.bottom_z))
|
||||
|
||||
@@ -14,6 +14,7 @@ else:
|
||||
from gears import planet_center_xy
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_standard_planet_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
@@ -34,6 +35,7 @@ def make_standard_planet_bearing_rassembly(
|
||||
return bearing
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_main_bearing_rassembly(
|
||||
*,
|
||||
bearing_id: str,
|
||||
@@ -48,11 +50,16 @@ def make_main_bearing_rassembly(
|
||||
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
|
||||
bearing_tag_prefix = ".".join(
|
||||
f"size{token}" if token[0].isdigit() else token
|
||||
for token in bearing_id.split("_")
|
||||
)
|
||||
inner_ring = make_annulus_rsolid(
|
||||
outer_radius=inner_outer_radius,
|
||||
inner_radius=bore_radius,
|
||||
bottom_z=-spec.width / 2.0,
|
||||
height=spec.width,
|
||||
tag_prefix=f"bearing.{bearing_tag_prefix}.inner.ring",
|
||||
tags=("role.bearing_inner_ring",),
|
||||
)
|
||||
outer_ring = make_annulus_rsolid(
|
||||
@@ -60,12 +67,17 @@ def make_main_bearing_rassembly(
|
||||
inner_radius=outer_inner_radius,
|
||||
bottom_z=-spec.width / 2.0,
|
||||
height=spec.width,
|
||||
tag_prefix=f"bearing.{bearing_tag_prefix}.outer.ring",
|
||||
tags=("role.bearing_outer_ring",),
|
||||
)
|
||||
ball = scad.make_sphere_rsolid(
|
||||
radius=ball_radius,
|
||||
center=(pitch_radius, 0.0, 0.0),
|
||||
)
|
||||
ball = scad.apply_tag(
|
||||
shape=ball,
|
||||
tag=f"solid.bearing.{bearing_tag_prefix}.rolling.element",
|
||||
)
|
||||
inner_part = make_axis_part_rpart(
|
||||
part_id=f"{bearing_id}_inner_ring",
|
||||
body=inner_ring,
|
||||
@@ -148,12 +160,14 @@ def make_main_bearing_rassembly(
|
||||
return bearing
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_planet_bearing_rplacement(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
|
||||
@@ -9,6 +9,7 @@ import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def apply_tags(*, shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
|
||||
"""Apply semantic tags through the public functional API."""
|
||||
|
||||
@@ -18,12 +19,14 @@ def apply_tags(*, shape: scad.Solid, tags: Iterable[str]) -> scad.Solid:
|
||||
return tagged
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_annulus_rsolid(
|
||||
*,
|
||||
outer_radius: float,
|
||||
inner_radius: float,
|
||||
bottom_z: float,
|
||||
height: float,
|
||||
tag_prefix: str,
|
||||
tags: Iterable[str],
|
||||
) -> scad.Solid:
|
||||
"""Create a strict single-solid annular cylinder."""
|
||||
@@ -33,17 +36,22 @@ def make_annulus_rsolid(
|
||||
height=height,
|
||||
bottom_face_center=(0.0, 0.0, bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.outer",
|
||||
result_tag=f"feature.{tag_prefix}.outer",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"{tag_prefix}.bore",
|
||||
result_tag=f"tool.{tag_prefix}.bore",
|
||||
)
|
||||
annulus = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
return apply_tags(shape=annulus, tags=tags)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_axis_part_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
@@ -70,6 +78,7 @@ def make_axis_part_rpart(
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_z_rotation_rplacement(
|
||||
*,
|
||||
origin: tuple[float, float, float],
|
||||
@@ -94,6 +103,7 @@ def radial_centers(*, count: int, radius: float, angle_offset: float = 0.0):
|
||||
yield index, angle_degrees, (radius * math.cos(angle), radius * math.sin(angle))
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_axial_hole_cutters_rsolids(
|
||||
*,
|
||||
count: int,
|
||||
@@ -101,12 +111,13 @@ def make_axial_hole_cutters_rsolids(
|
||||
hole_radius: float,
|
||||
bottom_z: float,
|
||||
height: float,
|
||||
tag_prefix: str,
|
||||
angle_offset: float = 0.0,
|
||||
) -> list[scad.Solid]:
|
||||
"""Create equally spaced axial hole cutters."""
|
||||
|
||||
cutters = []
|
||||
for _index, _angle, center in radial_centers(
|
||||
for index, _angle, center in radial_centers(
|
||||
count=count,
|
||||
radius=pcd / 2.0,
|
||||
angle_offset=angle_offset,
|
||||
@@ -117,6 +128,8 @@ def make_axial_hole_cutters_rsolids(
|
||||
height=height,
|
||||
bottom_face_center=(center[0], center[1], bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"{tag_prefix}.hole{index + 1}",
|
||||
result_tag=f"tool.{tag_prefix}.hole{index + 1}",
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
@@ -126,9 +139,13 @@ 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()
|
||||
local_roles = [
|
||||
tag
|
||||
for tag in scad.list_tags(shape=solid, scope="local")
|
||||
if tag.startswith("role.")
|
||||
]
|
||||
print(
|
||||
f"{label}: faces={len(faces)} role_faces={len(role_faces)} "
|
||||
f"{label}: faces={len(faces)} local_roles={len(local_roles)} "
|
||||
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
|
||||
)
|
||||
|
||||
@@ -142,6 +159,7 @@ def ground_compound(*, label: str, compound: scad.Compound) -> None:
|
||||
print(f"{label}: solids={len(solids)} faces={faces} volume={volume:.3f}")
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def connector_ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
"""Create a component-scoped connector reference."""
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ POWER_CAN_TERMINAL_CENTER = (11.0, 0.0)
|
||||
MOSFET_ANGLES = (22.5, 67.5, 112.5, 202.5, 247.5, 292.5)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_integrated_controller_rassembly(
|
||||
*,
|
||||
pcb_material: scad.Material,
|
||||
@@ -139,12 +140,15 @@ def make_integrated_controller_rassembly(
|
||||
return controller
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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),
|
||||
tag_prefix="controller.pcb.board",
|
||||
result_tag="feature.controller.pcb.board",
|
||||
)
|
||||
cutters: list[scad.Solid] = [
|
||||
scad.make_cylinder_rsolid(
|
||||
@@ -152,6 +156,8 @@ def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, PCB_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix="controller.pcb.center.bore",
|
||||
result_tag="tool.controller.pcb.center.bore",
|
||||
)
|
||||
]
|
||||
cutters.extend(
|
||||
@@ -161,6 +167,7 @@ def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=PCB_MOUNT_HOLE_RADIUS,
|
||||
bottom_z=PCB_BOTTOM_Z - 1.0,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
tag_prefix="controller.pcb.mount.hole",
|
||||
angle_offset=45.0,
|
||||
)
|
||||
)
|
||||
@@ -171,9 +178,13 @@ def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=3.45,
|
||||
bottom_z=PCB_BOTTOM_Z - 1.0,
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
tag_prefix="controller.pcb.rear.column.clearance",
|
||||
)
|
||||
)
|
||||
for x, count in ((PHASE_TERMINAL_CENTER[0], 3), (POWER_CAN_TERMINAL_CENTER[0], 4)):
|
||||
for terminal_id, x, count in (
|
||||
("phase", PHASE_TERMINAL_CENTER[0], 3),
|
||||
("power.can", POWER_CAN_TERMINAL_CENTER[0], 4),
|
||||
):
|
||||
for pin in range(count):
|
||||
y = (pin - (count - 1) / 2.0) * 1.8
|
||||
cutters.append(
|
||||
@@ -182,6 +193,8 @@ def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=PCB_THICKNESS + 2.0,
|
||||
bottom_face_center=(x, y, PCB_BOTTOM_Z - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"controller.pcb.terminal.{terminal_id}.pin{pin + 1}",
|
||||
result_tag=f"tool.controller.pcb.terminal.{terminal_id}.pin{pin + 1}",
|
||||
)
|
||||
)
|
||||
board = scad.cut_rsolid(board, cutters, skip_non_intersecting=False)
|
||||
@@ -216,12 +229,15 @@ def _make_controller_pcb_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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),
|
||||
tag_prefix="controller.mosfet.package",
|
||||
result_tag="feature.controller.mosfet.package",
|
||||
)
|
||||
package = apply_tags(shape=package, tags=("role.power_mosfet", "group.three_phase_bridge"))
|
||||
return make_axis_part_rpart(
|
||||
@@ -233,6 +249,7 @@ def _make_mosfet_package_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _make_terminal_block_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
@@ -241,11 +258,14 @@ def _make_terminal_block_rpart(
|
||||
pin_count: int,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
terminal_tag_prefix = part_id.replace("_", ".")
|
||||
body = scad.make_box_rsolid(
|
||||
width=width,
|
||||
height=6.0,
|
||||
depth=4.9,
|
||||
bottom_face_center=(0.0, 0.0, 0.0),
|
||||
tag_prefix=f"controller.terminal.{terminal_tag_prefix}.body",
|
||||
result_tag=f"feature.controller.terminal.{terminal_tag_prefix}.body",
|
||||
)
|
||||
access_cutters = []
|
||||
for pin in range(pin_count):
|
||||
@@ -256,6 +276,8 @@ def _make_terminal_block_rpart(
|
||||
height=width + 2.0,
|
||||
bottom_face_center=(-width / 2.0 - 1.0, y, 2.45),
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
tag_prefix=f"controller.terminal.{terminal_tag_prefix}.access{pin + 1}",
|
||||
result_tag=f"tool.controller.terminal.{terminal_tag_prefix}.access{pin + 1}",
|
||||
)
|
||||
)
|
||||
body = scad.cut_rsolid(body, access_cutters, skip_non_intersecting=False)
|
||||
|
||||
@@ -110,6 +110,7 @@ except ImportError: # Support direct execution from this example directory.
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_ring_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -128,17 +129,25 @@ def make_stage_ring_gear_rpart(
|
||||
addendum_factor=ADDENDUM_FACTOR,
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
)
|
||||
ring = scad.apply_tag(
|
||||
shape=ring,
|
||||
tag=f"solid.stdlib.{stage.stage_id}.fixed.herringbone.ring.gear",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.ring.support",
|
||||
result_tag=f"feature.reducer.{stage.stage_id}.ring.support",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.ring.support.bore",
|
||||
result_tag=f"tool.reducer.{stage.stage_id}.ring.support.bore",
|
||||
)
|
||||
support = scad.cut_rsolid(support, support_bore, skip_non_intersecting=False)
|
||||
ring = scad.union_rsolid(ring, support, glue=False)
|
||||
@@ -159,6 +168,7 @@ def make_stage_ring_gear_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage_planet_gear_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -176,12 +186,18 @@ def make_stage_planet_gear_rpart(
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
planet = scad.apply_tag(
|
||||
shape=planet,
|
||||
tag=f"solid.stdlib.{stage.stage_id}.reusable.herringbone.planet.gear",
|
||||
)
|
||||
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),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.planet.bearing.seat",
|
||||
result_tag=f"tool.reducer.{stage.stage_id}.planet.bearing.seat",
|
||||
)
|
||||
planet = scad.cut_rsolid(planet, bearing_seat, skip_non_intersecting=False)
|
||||
planet = apply_tags(
|
||||
@@ -205,6 +221,7 @@ def make_stage_planet_gear_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the first carrier and integral second-stage sun/shaft."""
|
||||
|
||||
@@ -223,6 +240,8 @@ def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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),
|
||||
tag_prefix="reducer.stage1.carrier.interstage.shaft",
|
||||
result_tag="feature.reducer.stage1.carrier.interstage.shaft",
|
||||
)
|
||||
stage2_sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=STAGE_2.sun_teeth,
|
||||
@@ -234,6 +253,10 @@ def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
stage2_sun = scad.apply_tag(
|
||||
shape=stage2_sun,
|
||||
tag="solid.stdlib.stage2.integral.herringbone.sun.gear",
|
||||
)
|
||||
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(
|
||||
@@ -270,6 +293,7 @@ def make_stage1_carrier_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_output_carrier_flange_rpart(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -297,12 +321,16 @@ def make_output_carrier_flange_rpart(
|
||||
),
|
||||
bottom_face_center=(0.0, 0.0, STAGE2_CARRIER_BOTTOM_Z - 0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix="reducer.stage2.output.shaft",
|
||||
result_tag="feature.reducer.stage2.output.shaft",
|
||||
)
|
||||
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),
|
||||
tag_prefix="reducer.stage2.output.flange",
|
||||
result_tag="feature.reducer.stage2.output.flange",
|
||||
)
|
||||
output = scad.union_rsolid(carrier, shaft, flange, glue=False)
|
||||
output = scad.cut_rsolid(
|
||||
@@ -313,6 +341,7 @@ def make_output_carrier_flange_rpart(
|
||||
hole_radius=OUTPUT_LINK_TAP_RADIUS,
|
||||
bottom_z=OUTPUT_FLANGE_TOP_Z - OUTPUT_LINK_THREAD_DEPTH,
|
||||
height=OUTPUT_LINK_THREAD_DEPTH + 1.0,
|
||||
tag_prefix="reducer.stage2.output.flange.thread",
|
||||
angle_offset=OUTPUT_LINK_BOLT_ANGLES_DEGREES[0],
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
@@ -351,6 +380,7 @@ def make_output_carrier_flange_rpart(
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_planet_rplacement(*, stage: StageSpec, index: int) -> scad.Placement:
|
||||
"""Place and visually phase one planet at its pitch center."""
|
||||
|
||||
@@ -377,6 +407,7 @@ def planet_center_xy(*, stage: StageSpec, index: int) -> tuple[float, float]:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def _make_carrier_body_rsolid(
|
||||
*,
|
||||
stage: StageSpec,
|
||||
@@ -393,6 +424,8 @@ def _make_carrier_body_rsolid(
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(0.0, 0.0, plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.carrier.hub",
|
||||
result_tag=f"feature.reducer.{stage.stage_id}.carrier.hub",
|
||||
)
|
||||
solids = [hub]
|
||||
pin_height = plate_bottom_z + plate_thickness - pin_bottom_z
|
||||
@@ -408,6 +441,8 @@ def _make_carrier_body_rsolid(
|
||||
height=arm_width,
|
||||
depth=plate_thickness,
|
||||
bottom_face_center=(arm_center_radius, 0.0, plate_bottom_z),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.carrier.arm{index + 1}",
|
||||
result_tag=f"feature.reducer.{stage.stage_id}.carrier.arm{index + 1}",
|
||||
)
|
||||
solids.append(
|
||||
scad.rotate_shape(
|
||||
@@ -423,6 +458,8 @@ def _make_carrier_body_rsolid(
|
||||
height=plate_thickness,
|
||||
bottom_face_center=(center[0], center[1], plate_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.carrier.pad{index + 1}",
|
||||
result_tag=f"feature.reducer.{stage.stage_id}.carrier.pad{index + 1}",
|
||||
)
|
||||
)
|
||||
solids.append(
|
||||
@@ -431,6 +468,8 @@ def _make_carrier_body_rsolid(
|
||||
height=pin_height,
|
||||
bottom_face_center=(center[0], center[1], pin_bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"reducer.{stage.stage_id}.carrier.pin{index + 1}",
|
||||
result_tag=f"feature.reducer.{stage.stage_id}.carrier.pin{index + 1}",
|
||||
)
|
||||
)
|
||||
carrier = scad.union_rsolid(solids, glue=False)
|
||||
|
||||
@@ -102,6 +102,7 @@ except ImportError: # Support direct execution from this example directory.
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the stator sleeve, front attachment land, and rear columns."""
|
||||
|
||||
@@ -110,6 +111,7 @@ def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=MOTOR_SHELL_INNER_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_BOTTOM_Z,
|
||||
height=MOTOR_SHELL_TOP_Z - MOTOR_SHELL_BOTTOM_Z,
|
||||
tag_prefix="housing.motor.shell.sleeve",
|
||||
tags=("role.motor_shell", "role.stator_thermal_path"),
|
||||
)
|
||||
front_land = make_annulus_rsolid(
|
||||
@@ -117,16 +119,19 @@ def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_TOP_Z - 1.4,
|
||||
height=1.4,
|
||||
tag_prefix="housing.motor.shell.front.land",
|
||||
tags=("role.motor_reducer_mount",),
|
||||
)
|
||||
columns = []
|
||||
for _index, _angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
|
||||
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),
|
||||
tag_prefix=f"housing.motor.shell.rear.column{index + 1}",
|
||||
result_tag=f"feature.housing.motor.shell.rear.column{index + 1}",
|
||||
)
|
||||
)
|
||||
shell = scad.union_rsolid(sleeve, front_land, columns, glue=False)
|
||||
@@ -138,6 +143,7 @@ def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_TOP_Z - 2.8,
|
||||
height=3.6,
|
||||
tag_prefix="housing.motor.shell.interface.clearance",
|
||||
angle_offset=30.0,
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
@@ -146,6 +152,7 @@ def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=MOTOR_SHELL_BOTTOM_Z - 1.0,
|
||||
height=10.4,
|
||||
tag_prefix="housing.motor.shell.rear.fastener.clearance",
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
@@ -175,6 +182,7 @@ def make_motor_shell_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the reducer sleeve and front motor-bearing bulkhead."""
|
||||
|
||||
@@ -183,6 +191,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=REDUCER_HOUSING_INNER_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_BOTTOM_Z,
|
||||
height=REDUCER_HOUSING_FRONT_Z - REDUCER_HOUSING_BOTTOM_Z,
|
||||
tag_prefix="housing.reducer.sleeve",
|
||||
tags=("role.reducer_housing_sleeve",),
|
||||
)
|
||||
bulkhead = make_annulus_rsolid(
|
||||
@@ -190,6 +199,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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,
|
||||
tag_prefix="housing.reducer.front.bulkhead",
|
||||
tags=("role.motor_front_bearing_bulkhead",),
|
||||
)
|
||||
interstage_divider = make_annulus_rsolid(
|
||||
@@ -197,6 +207,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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,
|
||||
tag_prefix="housing.reducer.interstage.divider",
|
||||
tags=("role.interstage_bearing_divider",),
|
||||
)
|
||||
output_mount_land = make_annulus_rsolid(
|
||||
@@ -204,6 +215,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=HOUSING_INTERFACE_LAND_INNER_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
|
||||
height=2.2,
|
||||
tag_prefix="housing.reducer.output.mount.land",
|
||||
tags=("role.output_cap_mount_land",),
|
||||
)
|
||||
housing = scad.union_rsolid(
|
||||
@@ -221,6 +233,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_BOTTOM_Z - 1.0,
|
||||
height=STAGE_1.bottom_z - REDUCER_HOUSING_BOTTOM_Z + 2.0,
|
||||
tag_prefix="housing.reducer.motor.interface.clearance",
|
||||
angle_offset=30.0,
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
@@ -229,6 +242,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=REDUCER_HOUSING_FRONT_Z - 2.2,
|
||||
height=3.2,
|
||||
tag_prefix="housing.reducer.output.interface.clearance",
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
@@ -265,6 +279,7 @@ def make_reducer_housing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create a four-arm removable rear motor-bearing support."""
|
||||
|
||||
@@ -274,15 +289,18 @@ def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=8.05,
|
||||
bottom_z=bottom_z,
|
||||
height=5.0,
|
||||
tag_prefix="housing.rear.spider.bearing.hub",
|
||||
tags=("role.rear_motor_bearing_seat",),
|
||||
)
|
||||
solids = [hub]
|
||||
for _index, angle, center in radial_centers(count=4, radius=REAR_COLUMN_PCD / 2.0):
|
||||
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),
|
||||
tag_prefix=f"housing.rear.spider.arm{index + 1}",
|
||||
result_tag=f"feature.housing.rear.spider.arm{index + 1}",
|
||||
)
|
||||
solids.append(
|
||||
scad.rotate_shape(
|
||||
@@ -298,6 +316,8 @@ def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=5.0,
|
||||
bottom_face_center=(center[0], center[1], bottom_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix=f"housing.rear.spider.boss{index + 1}",
|
||||
result_tag=f"feature.housing.rear.spider.boss{index + 1}",
|
||||
)
|
||||
)
|
||||
spider = scad.union_rsolid(solids, glue=False)
|
||||
@@ -309,6 +329,7 @@ def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=bottom_z - 1.0,
|
||||
height=7.0,
|
||||
tag_prefix="housing.rear.spider.fastener.clearance",
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
@@ -329,6 +350,7 @@ def make_rear_bearing_spider_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the rear cover with PCB standoffs and terminal apertures."""
|
||||
|
||||
@@ -337,15 +359,23 @@ def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=REAR_COVER_THICKNESS,
|
||||
bottom_face_center=(0.0, 0.0, REAR_COVER_BOTTOM_Z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
tag_prefix="housing.rear.cover.plate",
|
||||
result_tag="feature.housing.rear.cover.plate",
|
||||
)
|
||||
standoffs = []
|
||||
for _index, _angle, center in radial_centers(count=4, radius=PCB_STANDOFF_PCD / 2.0, angle_offset=45.0):
|
||||
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),
|
||||
tag_prefix=f"housing.rear.cover.pcb.standoff{index + 1}",
|
||||
result_tag=f"feature.housing.rear.cover.pcb.standoff{index + 1}",
|
||||
)
|
||||
)
|
||||
cover = scad.union_rsolid(cover, standoffs, glue=False)
|
||||
@@ -354,18 +384,24 @@ def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=7.2,
|
||||
depth=REAR_COVER_THICKNESS + 2.0,
|
||||
bottom_face_center=(-11.0, 0.0, REAR_COVER_BOTTOM_Z - 1.0),
|
||||
tag_prefix="housing.rear.cover.phase.aperture",
|
||||
result_tag="tool.housing.rear.cover.phase.aperture",
|
||||
)
|
||||
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),
|
||||
tag_prefix="housing.rear.cover.power.can.aperture",
|
||||
result_tag="tool.housing.rear.cover.power.can.aperture",
|
||||
)
|
||||
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),
|
||||
tag_prefix="housing.rear.cover.center.service",
|
||||
result_tag="tool.housing.rear.cover.center.service",
|
||||
)
|
||||
cover = scad.cut_rsolid(
|
||||
cover,
|
||||
@@ -378,6 +414,7 @@ def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=REAR_FASTENER_HOLE_RADIUS,
|
||||
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
|
||||
height=REAR_COVER_THICKNESS + 2.0,
|
||||
tag_prefix="housing.rear.cover.column.fastener.clearance",
|
||||
),
|
||||
make_axial_hole_cutters_rsolids(
|
||||
count=4,
|
||||
@@ -385,6 +422,7 @@ def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=1.1,
|
||||
bottom_z=REAR_COVER_BOTTOM_Z - 1.0,
|
||||
height=PCB_BOTTOM_Z - REAR_COVER_BOTTOM_Z + 2.0,
|
||||
tag_prefix="housing.rear.cover.pcb.fastener.clearance",
|
||||
angle_offset=45.0,
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
@@ -408,6 +446,7 @@ def make_rear_electronics_cover_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the removable paired-bearing cartridge and front cap."""
|
||||
|
||||
@@ -417,6 +456,7 @@ def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=bearing_clearance_radius,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z,
|
||||
height=3.0,
|
||||
tag_prefix="housing.output.cap.rear.flange",
|
||||
tags=("role.output_cap_mount_flange",),
|
||||
)
|
||||
cartridge = make_annulus_rsolid(
|
||||
@@ -424,6 +464,7 @@ def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=bearing_clearance_radius,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z,
|
||||
height=OUTPUT_CAP_CARTRIDGE_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 0.1,
|
||||
tag_prefix="housing.output.cap.bearing.cartridge",
|
||||
tags=("role.paired_output_bearing_seat",),
|
||||
)
|
||||
bearing_retainer = make_annulus_rsolid(
|
||||
@@ -431,6 +472,7 @@ def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
inner_radius=8.10,
|
||||
bottom_z=OUTPUT_CAP_CARTRIDGE_TOP_Z - 0.1,
|
||||
height=0.5,
|
||||
tag_prefix="housing.output.cap.bearing.retainer",
|
||||
tags=("role.output_axial_retainer",),
|
||||
)
|
||||
outer_lip = make_annulus_rsolid(
|
||||
@@ -438,6 +480,7 @@ def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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,
|
||||
tag_prefix="housing.output.cap.labyrinth.lip",
|
||||
tags=("role.output_labyrinth_lip",),
|
||||
)
|
||||
cap = scad.union_rsolid(rear_flange, cartridge, bearing_retainer, outer_lip, glue=False)
|
||||
@@ -449,6 +492,7 @@ def make_output_bearing_cap_rpart(*, material: scad.Material) -> scad.Part:
|
||||
hole_radius=M3_CLEARANCE_RADIUS,
|
||||
bottom_z=OUTPUT_CAP_BOTTOM_Z - 1.0,
|
||||
height=OUTPUT_CAP_TOP_Z - OUTPUT_CAP_BOTTOM_Z + 2.0,
|
||||
tag_prefix="housing.output.cap.interface.clearance",
|
||||
),
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Build, validate, replay, and export the integrated BLDC joint actuator."""
|
||||
"""Build and export the integrated BLDC joint actuator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,19 +8,34 @@ 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
|
||||
try:
|
||||
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
|
||||
except ImportError: # Support direct execution from this example directory.
|
||||
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)
|
||||
@@ -28,18 +43,21 @@ sys.setrecursionlimit(30000)
|
||||
OUT_DIR = Path("examples/out/integrated_bldc_joint_actuator")
|
||||
|
||||
|
||||
@scad.model(graph_id="integrated_50mm_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
|
||||
assembly = make_integrated_bldc_joint_actuator_rassembly(materials=materials)
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
preview = scad.apply_tag(
|
||||
shape=preview,
|
||||
tag="scene.integrated.bldc.joint.actuator.preview",
|
||||
)
|
||||
ground_compound(label="integrated_actuator_preview", compound=preview)
|
||||
scad.capture_result(value=(assembly, preview))
|
||||
return assembly, preview
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -53,19 +71,18 @@ def main() -> None:
|
||||
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")
|
||||
result = build_integrated_bldc_joint_actuator()
|
||||
assembly, preview = result.value
|
||||
model_path.write_text(result.model_json, encoding="utf-8")
|
||||
session_path.write_text(result.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)
|
||||
payload = json.loads(result.model_json)
|
||||
|
||||
fcstd_status = "not attempted"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
json_str=model_json,
|
||||
json_str=result.model_json,
|
||||
output_path=str(fcstd_path.resolve()),
|
||||
document_name="Integrated50mmBLDCJointActuator",
|
||||
freecad_cmd=None,
|
||||
@@ -84,9 +101,6 @@ def main() -> None:
|
||||
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}")
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_actuator_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create the materials used by Case 20."""
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ except ImportError: # Support direct execution from this example directory.
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_bldc_stator_rassembly(
|
||||
*,
|
||||
steel_material: scad.Material,
|
||||
@@ -130,6 +131,7 @@ def make_bldc_stator_rassembly(
|
||||
return stator
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
def make_bldc_rotor_rassembly(
|
||||
*,
|
||||
steel_material: scad.Material,
|
||||
@@ -190,23 +192,27 @@ def make_bldc_rotor_rassembly(
|
||||
return rotor
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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,
|
||||
tag_prefix="motor.stator.back.iron",
|
||||
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):
|
||||
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),
|
||||
tag_prefix=f"motor.stator.tooth{index + 1}",
|
||||
result_tag=f"feature.motor.stator.tooth{index + 1}",
|
||||
)
|
||||
teeth.append(
|
||||
scad.rotate_shape(
|
||||
@@ -255,6 +261,7 @@ def _make_stator_core_rpart(*, material: scad.Material) -> scad.Part:
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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
|
||||
@@ -263,24 +270,32 @@ def _make_winding_pack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
height=1.2,
|
||||
depth=side_depth,
|
||||
bottom_face_center=(17.55, 2.1, side_bottom_z),
|
||||
tag_prefix="motor.winding.side.positive",
|
||||
result_tag="feature.motor.winding.side.positive",
|
||||
)
|
||||
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),
|
||||
tag_prefix="motor.winding.side.negative",
|
||||
result_tag="feature.motor.winding.side.negative",
|
||||
)
|
||||
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),
|
||||
tag_prefix="motor.winding.rear.end.turn",
|
||||
result_tag="feature.motor.winding.rear.end.turn",
|
||||
)
|
||||
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),
|
||||
tag_prefix="motor.winding.front.end.turn",
|
||||
result_tag="feature.motor.winding.front.end.turn",
|
||||
)
|
||||
winding = scad.union_rsolid(
|
||||
side_positive,
|
||||
@@ -302,6 +317,7 @@ def _make_winding_pack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
)
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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(
|
||||
@@ -309,12 +325,16 @@ def _make_rotor_shaft_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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),
|
||||
tag_prefix="motor.rotor.drive.shaft",
|
||||
result_tag="feature.motor.rotor.drive.shaft",
|
||||
)
|
||||
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),
|
||||
tag_prefix="motor.rotor.back.iron",
|
||||
result_tag="feature.motor.rotor.back.iron",
|
||||
)
|
||||
sun = scad.std.gear.make_herringbone_gear_rsolid(
|
||||
n_teeth=STAGE_1.sun_teeth,
|
||||
@@ -326,6 +346,10 @@ def _make_rotor_shaft_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
clearance_factor=CLEARANCE_FACTOR,
|
||||
backlash=BACKLASH,
|
||||
)
|
||||
sun = scad.apply_tag(
|
||||
shape=sun,
|
||||
tag="solid.stdlib.stage1.integral.herringbone.sun.gear",
|
||||
)
|
||||
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(
|
||||
@@ -354,6 +378,7 @@ def _make_rotor_shaft_sun_rpart(*, material: scad.Material) -> scad.Part:
|
||||
return part
|
||||
|
||||
|
||||
@scad.requires_session
|
||||
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
|
||||
@@ -363,6 +388,8 @@ def _make_rotor_magnet_rpart(*, material: scad.Material) -> scad.Part:
|
||||
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),
|
||||
tag_prefix="motor.rotor.reusable.magnet",
|
||||
result_tag="feature.motor.rotor.reusable.magnet",
|
||||
)
|
||||
magnet = apply_tags(shape=magnet, tags=("role.rotor_magnet", "group.rotor_magnets"))
|
||||
print(
|
||||
|
||||
@@ -1,22 +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.
|
||||
Generated artifacts are written to `examples/out/`, which is ignored by git.
|
||||
|
||||
Replayable examples use one top-level `@scad.model` entry. That entry owns the
|
||||
single `GraphSession` and returns a `ModelResult`; reusable graph-producing
|
||||
builders use `@scad.requires_session`. Final outputs are selected with
|
||||
`scad.capture_result(...)`; `@scad.model(export_dir=...)` can automatically
|
||||
write one self-contained Scene ZIP after execution. It embeds the replayable
|
||||
model, mapped Python source files, render GLBs, and entity sidecars. Examples
|
||||
that call explicit export or translator APIs may also write STEP, JSON, or
|
||||
FreeCAD files.
|
||||
|
||||
## 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.
|
||||
- `04_dimension_tolerance_chain.py` — expression-driven dimension tolerance analysis and validation.
|
||||
- `08_constrained_sketch.py` — fully constrained sketch profiles, feature promotion, replay, and FreeCAD export.
|
||||
- `09_naca0016_blade_freecad.py` — NACA 0016 B-spline blade model JSON, STEP, and FreeCAD translation.
|
||||
- `10_part_assembly.py` — hydraulic rod assembly with sleeve/piston parts, prismatic motion, and automatic self-contained Scene ZIP export.
|
||||
- `16_compact_two_stage_planetary_reducer/` — modular 58.8 mm diameter, 30 mm tall, 20:1 two-stage herringbone planetary reducer with graph/model JSON replay, STEP export, solved constraints, and a static collision probe.
|
||||
- `20_integrated_bldc_joint_actuator/` — compact 50 mm OD joint actuator with a 12-slot/14-pole inner-rotor BLDC motor, two-stage planetary reducer, split housing, output bearings, and controller electronics.
|
||||
|
||||
Reference in New Issue
Block a user