first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
@@ -0,0 +1,163 @@
"""Assembly and kinematic constraints for a four-planet planetary reducer."""
from __future__ import annotations
import simplecadapi as scad
from dimensions import (
FIXED_RING_REDUCTION,
PLANET_COUNT,
PLANET_PITCH_RADIUS,
RING_PITCH_RADIUS,
SUN_PITCH_RADIUS,
)
from materials import make_materials_rdict
from parts import (
make_carrier_rpart,
make_planet_component_rplacement,
make_planet_gear_rpart,
make_ring_gear_rpart,
make_sun_gear_rpart,
)
def make_four_planet_planetary_reducer_rassembly() -> scad.Assembly:
"""Build and solve the exposed four-planet fixed-ring reducer gearset."""
print(
f"ratio_plan: fixed_ring={FIXED_RING_REDUCTION:.3f}:1 "
f"planets={PLANET_COUNT} sun_r={SUN_PITCH_RADIUS:.3f} "
f"planet_r={PLANET_PITCH_RADIUS:.3f} ring_r={RING_PITCH_RADIUS:.3f}"
)
materials = make_materials_rdict()
sun = make_sun_gear_rpart(material=materials["gear"])
ring = make_ring_gear_rpart(material=materials["ring"])
planet = make_planet_gear_rpart(material=materials["gear"])
carrier = make_carrier_rpart(material=materials["carrier"])
reducer = scad.make_assembly_rassembly(
assembly_id="four_planet_planetary_reducer",
name="Exposed 3.5:1 four-planet fixed-ring planetary reducer gearset",
)
for component_id, item, placement, name in (
("fixed_ring", ring, scad.identity_placement_rplacement(), "Fixed internal ring gear"),
("sun_input", sun, scad.identity_placement_rplacement(), "Input sun gear"),
("output_carrier", carrier, scad.identity_placement_rplacement(), "Four-pin output carrier"),
):
reducer = scad.add_component_rassembly(
assembly=reducer,
item=item,
component_id=component_id,
placement=placement,
name=name,
)
for index in range(PLANET_COUNT):
reducer = scad.add_component_rassembly(
assembly=reducer,
item=planet,
component_id=f"planet_{index + 1}",
placement=make_planet_component_rplacement(index=index),
name=f"Planet gear {index + 1}",
)
reducer = _add_public_connectors_rassembly(assembly=reducer)
reducer = _add_kinematic_constraints_rassembly(assembly=reducer)
reducer = scad.solve_assembly_constraints_rassembly(assembly=reducer, strict=True)
_ground_constraint_report(assembly=reducer)
print(
f"planetary_components: count={len(reducer.component_ids())} "
f"constraints={len(reducer.constraint_ids())}"
)
return reducer
def _add_public_connectors_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
forwarded = (
("fixed_axis", "fixed_ring", "axis", "Fixed ring datum"),
("input_axis", "sun_input", "axis", "Sun input datum"),
("output_axis", "output_carrier", "output_axis", "Carrier output datum"),
)
for connector_id, component_id, source_connector_id, name in forwarded:
assembly = scad.forward_connector_rassembly(
assembly=assembly,
connector_id=connector_id,
source_component_id=component_id,
source_connector_id=source_connector_id,
name=name,
)
print("public_connectors: " + ",".join(connector_id for connector_id, *_ in forwarded))
return assembly
def _add_kinematic_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="fixed_ring")
revolutes = (
("sun_input_revolute", "fixed_ring", "axis", "sun_input", "axis", 0.0),
("carrier_output_revolute", "fixed_ring", "axis", "output_carrier", "axis", 0.0),
)
for constraint_id, a_component, a_connector, b_component, b_connector, drive_angle in revolutes:
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=constraint_id,
connector_a=_ref(component_id=a_component, connector_id=a_connector),
connector_b=_ref(component_id=b_component, connector_id=b_connector),
drive_angle_degrees=drive_angle,
angle_limit=None,
name=constraint_id.replace("_", " "),
)
for index in range(PLANET_COUNT):
planet_id = f"planet_{index + 1}"
assembly = scad.add_revolute_constraint_rassembly(
assembly=assembly,
constraint_id=f"planet_{index + 1}_pin_revolute",
connector_a=_ref(component_id="output_carrier", connector_id=f"planet_{index + 1}_axis"),
connector_b=_ref(component_id=planet_id, connector_id="axis"),
drive_angle_degrees=None,
angle_limit=None,
name=f"Planet {index + 1} pin bearing revolute",
)
assembly = scad.add_gear_constraint_rassembly(
assembly=assembly,
constraint_id=f"sun_to_planet_{index + 1}_external_mesh",
connector_a=_ref(component_id="sun_input", connector_id="axis"),
connector_b=_ref(component_id=planet_id, connector_id="axis"),
pitch_radius_a=SUN_PITCH_RADIUS,
pitch_radius_b=PLANET_PITCH_RADIUS,
phase_offset=None,
name=f"Sun external mesh to planet {index + 1}",
)
assembly = scad.add_belt_constraint_rassembly(
assembly=assembly,
constraint_id=f"ring_to_planet_{index + 1}_internal_mesh",
connector_a=_ref(component_id="fixed_ring", connector_id="axis"),
connector_b=_ref(component_id=planet_id, connector_id="axis"),
pulley_radius_a=RING_PITCH_RADIUS,
pulley_radius_b=PLANET_PITCH_RADIUS,
phase_offset=None,
name=f"Fixed ring internal mesh to planet {index + 1}",
)
print(
f"constraints_added: grounded=1 revolute={2 + PLANET_COUNT} "
f"external_mesh={PLANET_COUNT} internal_mesh={PLANET_COUNT}"
)
return assembly
def _ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
return scad.make_connector_ref_rconnectorref(
component_id=component_id,
connector_id=connector_id,
)
def _ground_constraint_report(*, assembly: scad.Assembly) -> None:
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
print(
f"assembly_constraints: solved={report.solved} grounded={len(report.grounded_component_ids)} "
f"solved_components={len(report.solved_component_ids)} unsolved={len(report.unsolved_component_ids)}"
)
for residual in report.residuals:
print(
f"constraint_{residual.constraint_id}: translation={residual.translation_error:.6g} "
f"angle={residual.angular_error_degrees:.6g} ok={residual.within_tolerance}"
)
@@ -0,0 +1,78 @@
"""Shared helpers for Example 19."""
from __future__ import annotations
import math
import simplecadapi as scad
from simplecadapi import ql
def make_z_rotation_rplacement(
*,
origin: tuple[float, float, float],
angle_degrees: float,
) -> scad.Placement:
"""Create a placement rotated about local Z and translated to origin."""
angle = math.radians(angle_degrees)
return scad.make_placement_rplacement(
origin=origin,
x_axis=(math.cos(angle), math.sin(angle), 0.0),
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
)
def add_axis_connector_rpart(
*,
part: scad.Part,
connector_id: str,
origin: tuple[float, float, float],
name: str,
) -> scad.Part:
"""Attach a topology-free Z-axis datum connector to a part."""
connector = scad.make_placement_connector_rconnector(
connector_id=connector_id,
placement=scad.make_placement_rplacement(
origin=origin,
x_axis=(1.0, 0.0, 0.0),
y_axis=(0.0, 1.0, 0.0),
),
name=name,
)
return scad.add_connector_rpart(part=part, connector=connector)
def make_axis_part_rpart(
*,
part_id: str,
body: scad.Solid,
name: str,
material: scad.Material,
connector_specs: tuple[tuple[str, tuple[float, float, float], str], ...],
) -> scad.Part:
"""Wrap one solid as a part and attach named axis connectors."""
part = scad.make_part_rpart(part_id=part_id, body=body, name=name)
part = scad.assign_material_rpart(part=part, material=material)
for connector_id, origin, connector_name in connector_specs:
part = add_axis_connector_rpart(
part=part,
connector_id=connector_id,
origin=origin,
name=connector_name,
)
print(f"part_{part_id}: connectors={len(part.connectors)} volume={body.get_volume():.3f}")
return part
def ground_solid(*, label: str, solid: scad.Solid) -> None:
"""Print a small QL-grounded summary for a generated solid."""
faces = ql.select(items=solid.get_faces()).all()
edges = ql.select(items=solid.get_edges()).all()
print(
f"{label}: faces={len(faces)} edges={len(edges)} "
f"volume={solid.get_volume():.3f} tags={','.join(scad.list_tags(shape=solid))}"
)
@@ -0,0 +1,54 @@
"""Design constants for the four-planet single-stage planetary reducer."""
from __future__ import annotations
import math
MODULE = 1.5
PRESSURE_ANGLE = 20.0
GEAR_HEIGHT = 8.0
BACKLASH = 0.04
ADDENDUM_FACTOR = 1.0
CLEARANCE_FACTOR = 0.25
SUN_TEETH = 24
PLANET_TEETH = 18
PLANET_COUNT = 4
RING_TEETH = SUN_TEETH + 2 * PLANET_TEETH
SUN_PITCH_RADIUS = MODULE * SUN_TEETH / 2.0
PLANET_PITCH_RADIUS = MODULE * PLANET_TEETH / 2.0
RING_PITCH_RADIUS = MODULE * RING_TEETH / 2.0
PLANET_CENTER_RADIUS = SUN_PITCH_RADIUS + PLANET_PITCH_RADIUS
FIXED_RING_REDUCTION = 1.0 + RING_TEETH / SUN_TEETH
RING_RIM_THICKNESS = 4.0
GEAR_AXIS_Z = GEAR_HEIGHT / 2.0
SUN_BORE_RADIUS = 3.0
PLANET_PIN_RADIUS = 2.6
PLANET_PIN_CLEARANCE_RADIUS = 3.2
CARRIER_BOTTOM_Z = -5.0
CARRIER_THICKNESS = 4.0
CARRIER_HUB_RADIUS = 10.0
CARRIER_ARM_WIDTH = 6.0
CARRIER_PIN_BOSS_RADIUS = 5.2
CARRIER_PIN_HEIGHT = GEAR_HEIGHT + 6.0
def planet_angle_degrees(*, index: int) -> float:
"""Return the equally spaced carrier angle for one planet index."""
return 360.0 * index / PLANET_COUNT
def planet_center_xy(*, index: int) -> tuple[float, float]:
"""Return the XY pitch-center location for one planet."""
angle = math.radians(planet_angle_degrees(index=index))
return (
PLANET_CENTER_RADIUS * math.cos(angle),
PLANET_CENTER_RADIUS * math.sin(angle),
)
@@ -0,0 +1,71 @@
"""Build, solve, and export the four-planet planetary reducer gearset."""
from __future__ import annotations
import json
from pathlib import Path
import simplecadapi as scad
from assembly import make_four_planet_planetary_reducer_rassembly
OUT_DIR = Path("examples/out/19_four_planet_planetary_reducer")
def _build_four_planet_reducer():
with scad.GraphSession(graph_id="four_planet_planetary_reducer") as session:
assembly = make_four_planet_planetary_reducer_rassembly()
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
session_json = scad.export_session_json(session=session)
model_json = scad.export_model_json(session=session)
return assembly, preview, model_json, session_json
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
model_path = OUT_DIR / "four_planet_planetary_reducer.model.json"
session_path = OUT_DIR / "four_planet_planetary_reducer.session.json"
step_path = OUT_DIR / "four_planet_planetary_reducer.step"
fcstd_path = OUT_DIR / "four_planet_planetary_reducer.FCStd"
if fcstd_path.exists():
fcstd_path.unlink()
assembly, preview, model_json, session_json = _build_four_planet_reducer()
model_path.write_text(model_json, encoding="utf-8")
session_path.write_text(session_json, encoding="utf-8")
scad.export_step(shapes=preview, filename=str(step_path))
imported = scad.import_model_json(json_str=model_json)
replayed = scad.replay_model_json(json_str=model_json)
payload = json.loads(model_json)
fcstd_status = "not attempted"
try:
scad.translator.freecad_translator.translate_model_json_to_fcstd(
json_str=model_json,
output_path=str(fcstd_path.resolve()),
document_name="FourPlanetPlanetaryReducer",
freecad_cmd=None,
)
fcstd_status = f"{fcstd_path} ({fcstd_path.stat().st_size} bytes)"
except Exception as exc: # pragma: no cover - depends on local FreeCAD install
fcstd_status = f"skipped ({exc.__class__.__name__}: {exc})"
print(f"assembly={assembly.assembly_id}")
print("components=" + ",".join(assembly.component_ids()))
print("constraints=" + ",".join(assembly.constraint_ids()))
print(f"preview_solids={len(preview.get_solids())}")
print(f"preview_volume={preview.get_volume():.3f}")
print(f"imported_keys={','.join(sorted(imported.keys()))}")
print(f"replay_outputs={len(replayed)}")
print("replay_types=" + ",".join(type(item).__name__ for item in replayed))
print(f"graph_nodes={len(payload['graph']['nodes'])}")
print(f"model={model_path}")
print(f"session={session_path}")
print(f"step={step_path}")
print(f"fcstd={fcstd_status}")
if __name__ == "__main__":
main()
@@ -0,0 +1,35 @@
"""Materials for the four-planet planetary reducer example."""
from __future__ import annotations
import simplecadapi as scad
def make_materials_rdict() -> dict[str, scad.Material]:
"""Create simple material definitions for the exposed gearset."""
materials = {
"gear": scad.make_material_rmaterial(
material_id="case_hardened_steel",
name="Case hardened gear steel",
density=7.85e-6,
density_unit="kg/mm^3",
color=(0.70, 0.70, 0.74),
),
"ring": scad.make_material_rmaterial(
material_id="nitrided_internal_ring_steel",
name="Nitrided internal ring steel",
density=7.85e-6,
density_unit="kg/mm^3",
color=(0.44, 0.46, 0.50),
),
"carrier": scad.make_material_rmaterial(
material_id="aluminum_7075_t6",
name="7075-T6 aluminum carrier",
density=2.81e-6,
density_unit="kg/mm^3",
color=(0.14, 0.48, 0.70),
),
}
print("materials: " + ",".join(sorted(materials)))
return materials
@@ -0,0 +1,206 @@
"""Gear and carrier parts for the four-planet planetary reducer."""
from __future__ import annotations
import simplecadapi as scad
from common import ground_solid, make_axis_part_rpart, make_z_rotation_rplacement
from dimensions import (
ADDENDUM_FACTOR,
BACKLASH,
CARRIER_ARM_WIDTH,
CARRIER_BOTTOM_Z,
CARRIER_HUB_RADIUS,
CARRIER_PIN_BOSS_RADIUS,
CARRIER_PIN_HEIGHT,
CARRIER_THICKNESS,
CLEARANCE_FACTOR,
GEAR_AXIS_Z,
GEAR_HEIGHT,
MODULE,
PLANET_CENTER_RADIUS,
PLANET_COUNT,
PLANET_PIN_CLEARANCE_RADIUS,
PLANET_PIN_RADIUS,
PLANET_TEETH,
PRESSURE_ANGLE,
RING_RIM_THICKNESS,
RING_TEETH,
SUN_BORE_RADIUS,
SUN_TEETH,
planet_angle_degrees,
planet_center_xy,
)
def make_sun_gear_rpart(*, material: scad.Material) -> scad.Part:
"""Create the input sun gear with a service bore and axis connector."""
sun = scad.std.gear.make_spur_gear_rsolid(
n_teeth=SUN_TEETH,
module=MODULE,
pressure_angle=PRESSURE_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH,
)
bore = scad.make_cylinder_rsolid(
radius=SUN_BORE_RADIUS,
height=GEAR_HEIGHT + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
sun = scad.cut_rsolid(sun, bore, skip_non_intersecting=False)
sun = scad.apply_tag(shape=sun, tag="role.sun_input_gear")
ground_solid(label="sun_gear", solid=sun)
return make_axis_part_rpart(
part_id="sun_input_gear",
body=sun,
name="Input sun gear, 24 teeth",
material=material,
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Sun input axis"),),
)
def make_ring_gear_rpart(*, material: scad.Material) -> scad.Part:
"""Create the fixed internal ring gear without an enclosing housing."""
ring = scad.std.gear.make_spur_ring_gear_rsolid(
n_teeth=RING_TEETH,
module=MODULE,
pressure_angle=PRESSURE_ANGLE,
gear_height=GEAR_HEIGHT,
rim_thickness=RING_RIM_THICKNESS,
backlash=BACKLASH,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
)
ring = scad.apply_tag(shape=ring, tag="role.fixed_internal_ring_gear")
ground_solid(label="ring_gear", solid=ring)
return make_axis_part_rpart(
part_id="fixed_ring_gear",
body=ring,
name="Fixed internal ring gear, 60 teeth",
material=material,
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Fixed ring axis"),),
)
def make_planet_gear_rpart(*, material: scad.Material) -> scad.Part:
"""Create one reusable planet gear with a carrier-pin bore."""
planet = scad.std.gear.make_spur_gear_rsolid(
n_teeth=PLANET_TEETH,
module=MODULE,
pressure_angle=PRESSURE_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH,
)
bore = scad.make_cylinder_rsolid(
radius=PLANET_PIN_CLEARANCE_RADIUS,
height=GEAR_HEIGHT + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
planet = scad.cut_rsolid(planet, bore, skip_non_intersecting=False)
planet = scad.apply_tag(shape=planet, tag="role.reusable_planet_gear")
ground_solid(label="planet_gear", solid=planet)
return make_axis_part_rpart(
part_id="planet_gear",
body=planet,
name="Reusable planet gear, 18 teeth",
material=material,
connector_specs=(("axis", (0.0, 0.0, GEAR_AXIS_Z), "Planet spin axis"),),
)
def make_carrier_rpart(*, material: scad.Material) -> scad.Part:
"""Create the four-pin carrier output spider."""
hub = scad.make_cylinder_rsolid(
radius=CARRIER_HUB_RADIUS,
height=CARRIER_THICKNESS,
bottom_face_center=(0.0, 0.0, CARRIER_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
arms: list[scad.Solid] = []
pin_bosses: list[scad.Solid] = []
pins: list[scad.Solid] = []
for index in range(PLANET_COUNT):
angle = planet_angle_degrees(index=index)
x, y = planet_center_xy(index=index)
arm = scad.make_box_rsolid(
width=PLANET_CENTER_RADIUS + CARRIER_PIN_BOSS_RADIUS,
height=CARRIER_ARM_WIDTH,
depth=CARRIER_THICKNESS,
bottom_face_center=(PLANET_CENTER_RADIUS / 2.0, 0.0, CARRIER_BOTTOM_Z),
)
arms.append(
scad.rotate_shape(
shape=arm,
angle=angle,
axis=(0.0, 0.0, 1.0),
origin=(0.0, 0.0, 0.0),
)
)
pin_bosses.append(
scad.make_cylinder_rsolid(
radius=CARRIER_PIN_BOSS_RADIUS,
height=CARRIER_THICKNESS,
bottom_face_center=(x, y, CARRIER_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
)
pins.append(
scad.make_cylinder_rsolid(
radius=PLANET_PIN_RADIUS,
height=CARRIER_PIN_HEIGHT,
bottom_face_center=(x, y, CARRIER_BOTTOM_Z),
axis=(0.0, 0.0, 1.0),
)
)
carrier = scad.union_rsolid([hub, arms, pin_bosses, pins], glue=False)
center_bore = scad.make_cylinder_rsolid(
radius=SUN_BORE_RADIUS + 0.8,
height=CARRIER_THICKNESS + 2.0,
bottom_face_center=(0.0, 0.0, CARRIER_BOTTOM_Z - 1.0),
axis=(0.0, 0.0, 1.0),
)
carrier = scad.cut_rsolid(carrier, center_bore, skip_non_intersecting=False)
carrier = scad.apply_tag(shape=carrier, tag="role.four_pin_output_carrier")
ground_solid(label="carrier", solid=carrier)
connector_specs = [
("axis", (0.0, 0.0, GEAR_AXIS_Z), "Carrier output axis"),
("output_axis", (0.0, 0.0, GEAR_AXIS_Z), "Public output axis"),
]
connector_specs.extend(
(
f"planet_{index + 1}_axis",
(*planet_center_xy(index=index), GEAR_AXIS_Z),
f"Planet {index + 1} carrier pin axis",
)
for index in range(PLANET_COUNT)
)
return make_axis_part_rpart(
part_id="four_pin_output_carrier",
body=carrier,
name="Four-pin output carrier spider",
material=material,
connector_specs=tuple(connector_specs),
)
def make_planet_component_rplacement(*, index: int) -> scad.Placement:
"""Return the placement for one of the four equally spaced planets."""
angle = planet_angle_degrees(index=index)
x, y = planet_center_xy(index=index)
tooth_phase = angle + 180.0 - (180.0 / PLANET_TEETH)
print(
f"planet_{index + 1}: center=({x:.3f},{y:.3f},0.000) "
f"carrier_angle={angle:.1f} spin_phase={tooth_phase:.1f}"
)
return make_z_rotation_rplacement(origin=(x, y, 0.0), angle_degrees=tooth_phase)