487 lines
18 KiB
Python
487 lines
18 KiB
Python
"""Build a hydraulic rod assembly with separate sleeve and piston-rod parts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import simplecadapi as scad
|
|
from simplecadapi import ql
|
|
|
|
|
|
OUT_DIR = Path(__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 = [
|
|
("zplus", 0.0, 16.0),
|
|
("yplus", 16.0, 0.0),
|
|
("zminus", 0.0, -16.0),
|
|
("yminus", -16.0, 0.0),
|
|
]
|
|
|
|
@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 = _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 = _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 = _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 = _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 = _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,
|
|
rod_gland_flange,
|
|
rod_gland_nose,
|
|
base_cap,
|
|
rear_eye,
|
|
rear_eye_neck,
|
|
glue=False,
|
|
)
|
|
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),
|
|
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,
|
|
bolt_hole,
|
|
)
|
|
|
|
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 = _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 = _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 = _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 = _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 = _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,
|
|
piston_seal_groove,
|
|
piston_land_right,
|
|
chrome_rod,
|
|
rod_eye,
|
|
rod_eye_neck,
|
|
glue=False,
|
|
)
|
|
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(
|
|
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(
|
|
material_id="chrome_plated_steel",
|
|
name="Chrome plated steel",
|
|
density=7.85e-6,
|
|
density_unit="kg/mm^3",
|
|
color=(0.78, 0.80, 0.82),
|
|
)
|
|
|
|
sleeve_part = scad.make_part_rpart(
|
|
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
|
|
)
|
|
|
|
piston_rod_part = scad.make_part_rpart(
|
|
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]
|
|
)
|
|
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(
|
|
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(
|
|
assembly_id="hydraulic_rod_assembly", name="Hydraulic rod assembly"
|
|
)
|
|
hydraulic_assembly = scad.add_component_rassembly(
|
|
assembly=hydraulic_assembly,
|
|
item=sleeve_part,
|
|
component_id="outer_sleeve",
|
|
placement=scad.identity_placement_rplacement(),
|
|
)
|
|
hydraulic_assembly = scad.add_component_rassembly(
|
|
assembly=hydraulic_assembly,
|
|
item=piston_rod_part,
|
|
component_id="inner_piston_rod",
|
|
placement=scad.identity_placement_rplacement(),
|
|
)
|
|
hydraulic_assembly = scad.ground_component_rassembly(
|
|
assembly=hydraulic_assembly, component_id="outer_sleeve"
|
|
)
|
|
hydraulic_assembly = scad.add_prismatic_constraint_rassembly(
|
|
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(
|
|
lower_value=0.0, upper_value=100.0
|
|
),
|
|
)
|
|
hydraulic_assembly = scad.solve_assembly_constraints_rassembly(
|
|
assembly=hydraulic_assembly
|
|
)
|
|
|
|
preview = scad.make_compound_from_assembly_rcompound(
|
|
assembly=hydraulic_assembly
|
|
)
|
|
preview = scad.apply_tag(
|
|
shape=preview,
|
|
tag="assembly.hydraulic.preview",
|
|
)
|
|
return hydraulic_assembly, preview
|
|
|
|
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)
|
|
result = build_hydraulic_rod_assembly()
|
|
assembly, preview = result.value
|
|
|
|
fcstd_path = OUT_DIR / "hydraulic_rod_assembly.FCStd"
|
|
fcstd_status = "skipped"
|
|
try:
|
|
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(result.model_json)
|
|
face_count = len(ql.faces().resolve(preview))
|
|
print("assembly", assembly.assembly_id)
|
|
print("components", assembly.component_ids())
|
|
print("preview_solids", len(preview.get_solids()))
|
|
print("preview_faces", face_count)
|
|
print("preview_volume", round(preview.get_volume(), 3))
|
|
print("graph_nodes", len(payload["graph"]["nodes"]))
|
|
for path in result.artifact_paths.values():
|
|
print("wrote", path)
|
|
print("fcstd", fcstd_status)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|