1401 lines
60 KiB
Python
1401 lines
60 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..industrial import bolt_xy_positions, compute_industrial_parameters, build_industrial_assembly_topology
|
|
from ..models import (
|
|
AssemblyManifest,
|
|
ComponentManifest,
|
|
ConstraintManifest,
|
|
IndustrialDesignParameters,
|
|
ReducerInstance,
|
|
sha256_json,
|
|
utc_now_iso,
|
|
write_json,
|
|
write_model_json,
|
|
)
|
|
from ..placement import PlacementPlan, solve_reducer_placements
|
|
from .gear_factory import gear_spec_for_component, make_external_gear
|
|
from .simplecad_assembly import add_forwarded_connector, cref, require_assembly_api
|
|
from .simplecad_generator import (
|
|
CadGenerationError,
|
|
_export_assembled_component_steps,
|
|
export_model_session_json,
|
|
import_simplecadapi,
|
|
)
|
|
from .simplecad_parts import (
|
|
add_axis_connector,
|
|
internal_ring_phase_correction_degrees,
|
|
make_axis_part,
|
|
make_materials,
|
|
make_planet,
|
|
make_planet_placement,
|
|
make_ring,
|
|
make_z_rotation_placement,
|
|
planet_center_xy,
|
|
role_for_component,
|
|
)
|
|
|
|
|
|
def _ring_solid(
|
|
scad: Any,
|
|
*,
|
|
outer_radius: float,
|
|
inner_radius: float,
|
|
height: float,
|
|
bottom_z: float,
|
|
) -> Any:
|
|
outer = scad.make_cylinder_rsolid(
|
|
radius=outer_radius,
|
|
height=height,
|
|
bottom_face_center=(0.0, 0.0, bottom_z),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
bore = scad.make_cylinder_rsolid(
|
|
radius=inner_radius,
|
|
height=height + 2.0,
|
|
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
return scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
|
|
|
|
|
def _tag(scad: Any, *, shape: Any, instance: ReducerInstance, component_id: str, formula_ref: str) -> Any:
|
|
tagged = scad.apply_tag(shape=shape, tag=f"formula.run_id.{instance.run_id}")
|
|
tagged = scad.apply_tag(shape=tagged, tag=f"formula.node.{component_id}")
|
|
tagged = scad.apply_tag(shape=tagged, tag=f"formula.ref.{formula_ref}")
|
|
return tagged
|
|
|
|
|
|
def _cut_bolt_holes(
|
|
scad: Any,
|
|
*,
|
|
shape: Any,
|
|
params: IndustrialDesignParameters,
|
|
bottom_z: float,
|
|
height: float,
|
|
) -> Any:
|
|
result = shape
|
|
hole_radius = params.bolt_diameter_mm / 2.0 + params.bearing_seat_clearance_mm
|
|
for x, y in bolt_xy_positions(params):
|
|
hole = scad.make_cylinder_rsolid(
|
|
radius=hole_radius,
|
|
height=height + 2.0,
|
|
bottom_face_center=(x, y, bottom_z - 1.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
result = scad.cut_rsolid(result, hole, skip_non_intersecting=False)
|
|
return result
|
|
|
|
|
|
def _carrier_plate_shape(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
thickness: float,
|
|
bottom_z: float,
|
|
) -> Any:
|
|
p = instance.parameters
|
|
pin_clearance_radius = params.planet_pin_diameter_mm / 2.0 + params.bearing_seat_clearance_mm
|
|
sun_bore_radius = max(0.8, min(2.2, instance.derived.sun_pitch_radius_mm * 0.35)) + 0.35
|
|
center_bore_radius = max(
|
|
sun_bore_radius,
|
|
params.output_bearing_outer_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
|
|
)
|
|
disk_radius = (
|
|
instance.derived.planet_orbit_radius_mm
|
|
+ params.planet_bearing_outer_diameter_mm / 2.0
|
|
+ params.radial_clearance_mm
|
|
)
|
|
plate = scad.make_cylinder_rsolid(
|
|
radius=disk_radius,
|
|
height=thickness,
|
|
bottom_face_center=(0.0, 0.0, bottom_z),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
center_bore = scad.make_cylinder_rsolid(
|
|
radius=center_bore_radius,
|
|
height=thickness + 2.0,
|
|
bottom_face_center=(0.0, 0.0, bottom_z - 1.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
plate = scad.cut_rsolid(plate, center_bore, skip_non_intersecting=False)
|
|
for index in range(p.planet_count):
|
|
x, y = planet_center_xy(instance, index)
|
|
pin_hole = scad.make_cylinder_rsolid(
|
|
radius=pin_clearance_radius,
|
|
height=thickness + 2.0,
|
|
bottom_face_center=(x, y, bottom_z - 1.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
plate = scad.cut_rsolid(plate, pin_hole, skip_non_intersecting=False)
|
|
return plate
|
|
|
|
|
|
def _sun_bore_radius(instance: ReducerInstance) -> float:
|
|
return max(0.8, min(2.2, instance.derived.sun_pitch_radius_mm * 0.35))
|
|
|
|
|
|
def make_industrial_sun(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
material: Any,
|
|
) -> Any:
|
|
p = instance.parameters
|
|
spec = gear_spec_for_component(instance, "sun")
|
|
body = make_external_gear(scad, spec)
|
|
bore_radius = _sun_bore_radius(instance)
|
|
bore = scad.make_cylinder_rsolid(
|
|
radius=bore_radius,
|
|
height=p.face_width_mm + 2.0,
|
|
bottom_face_center=(0.0, 0.0, -1.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
body = scad.cut_rsolid(body, bore, skip_non_intersecting=False)
|
|
slot_height = params.sun_key_height_mm + 2.0 * params.bearing_seat_clearance_mm
|
|
slot_width = params.sun_key_width_mm + 2.0 * params.bearing_seat_clearance_mm
|
|
keyway = scad.make_box_rsolid(
|
|
width=slot_height + 0.20,
|
|
height=slot_width,
|
|
depth=p.face_width_mm + 2.0,
|
|
bottom_face_center=(
|
|
bore_radius + slot_height / 2.0,
|
|
0.0,
|
|
-1.0,
|
|
),
|
|
)
|
|
body = scad.cut_rsolid(body, keyway, skip_non_intersecting=False)
|
|
body = _tag(scad, shape=body, instance=instance, component_id="sun", formula_ref="kinematic.sun.keyway")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id="sun_gear",
|
|
body=body,
|
|
name=f"Input {p.tooth_form} sun gear with keyed bore {p.z_sun}t m={p.module_mm}",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, p.face_width_mm / 2.0), "Sun keyed bore axis"),),
|
|
)
|
|
|
|
|
|
def make_input_key(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
material: Any,
|
|
) -> Any:
|
|
p = instance.parameters
|
|
shaft_radius = params.input_shaft_diameter_mm / 2.0
|
|
key_bottom_z = (p.face_width_mm - params.sun_key_length_mm) / 2.0
|
|
body = scad.make_box_rsolid(
|
|
width=params.sun_key_height_mm,
|
|
height=params.sun_key_width_mm,
|
|
depth=params.sun_key_length_mm,
|
|
bottom_face_center=(
|
|
shaft_radius + params.sun_key_height_mm / 2.0 + params.bearing_seat_clearance_mm,
|
|
0.0,
|
|
key_bottom_z,
|
|
),
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id="sun_input_key", formula_ref="industrial.sun_input_key")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id="sun_input_key",
|
|
body=body,
|
|
name="Input shaft parallel key for sun gear",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, p.face_width_mm / 2.0), "Input key reference axis"),),
|
|
)
|
|
|
|
|
|
def make_industrial_carrier_body(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
material: Any,
|
|
) -> Any:
|
|
bottom_z = -params.axial_clearance_mm - params.carrier_plate_thickness_mm
|
|
body = _carrier_plate_shape(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
thickness=params.carrier_plate_thickness_mm,
|
|
bottom_z=bottom_z,
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id="carrier", formula_ref="industrial.carrier_body")
|
|
connector_specs = [
|
|
("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier motion axis"),
|
|
("output_axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier output axis"),
|
|
]
|
|
connector_specs.extend(
|
|
(
|
|
f"planet_{index + 1}_axis",
|
|
(*planet_center_xy(instance, index), instance.parameters.face_width_mm / 2.0),
|
|
f"Planet {index + 1} pin axis",
|
|
)
|
|
for index in range(instance.parameters.planet_count)
|
|
)
|
|
return make_axis_part(
|
|
scad,
|
|
part_id="carrier",
|
|
body=body,
|
|
name="Industrial carrier rear plate/body",
|
|
material=material,
|
|
connector_specs=tuple(connector_specs),
|
|
)
|
|
|
|
|
|
def make_carrier_front_plate(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
material: Any,
|
|
) -> Any:
|
|
bottom_z = instance.parameters.face_width_mm + params.axial_clearance_mm
|
|
body = _carrier_plate_shape(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
thickness=params.carrier_front_plate_thickness_mm,
|
|
bottom_z=bottom_z,
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id="carrier_front_plate", formula_ref="industrial.carrier_front_plate")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id="carrier_front_plate",
|
|
body=body,
|
|
name="Industrial carrier front retention plate",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Carrier front plate axis"),),
|
|
)
|
|
|
|
|
|
def make_pin(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
index: int,
|
|
material: Any,
|
|
) -> Any:
|
|
bottom_z = -params.axial_clearance_mm - params.carrier_plate_thickness_mm + 0.05
|
|
top_z = instance.parameters.face_width_mm + params.axial_clearance_mm + params.carrier_front_plate_thickness_mm - 0.05
|
|
body = scad.make_cylinder_rsolid(
|
|
radius=params.planet_pin_diameter_mm / 2.0,
|
|
height=top_z - bottom_z,
|
|
bottom_face_center=(0.0, 0.0, bottom_z),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
component_id = f"planet_pin_{index}"
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=f"Planet pin {index}",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Planet pin {index} axis"),),
|
|
)
|
|
|
|
|
|
def make_bearing_ring(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
component_id: str,
|
|
inner_diameter: float,
|
|
outer_diameter: float,
|
|
width: float,
|
|
center_z: float,
|
|
material: Any,
|
|
formula_ref: str,
|
|
) -> Any:
|
|
body = _ring_solid(
|
|
scad,
|
|
outer_radius=outer_diameter / 2.0,
|
|
inner_radius=inner_diameter / 2.0,
|
|
height=width,
|
|
bottom_z=center_z - width / 2.0,
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=formula_ref)
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=component_id.replace("_", " ").title(),
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, center_z), f"{component_id} axis"),),
|
|
)
|
|
|
|
|
|
def make_ball_bearing_part(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
component_id: str,
|
|
bore_diameter: float,
|
|
outer_diameter: float,
|
|
width: float,
|
|
center_z: float,
|
|
ball_count: int,
|
|
material: Any,
|
|
formula_ref: str,
|
|
) -> Any:
|
|
bore_radius = bore_diameter / 2.0
|
|
outer_radius = outer_diameter / 2.0
|
|
ball_radius = params.bearing_ball_diameter_mm / 2.0
|
|
raceway_clearance = max(0.01, params.bearing_seat_clearance_mm * 0.3)
|
|
ball_pitch_radius = (bore_radius + outer_radius) / 2.0
|
|
inner_ring_outer_radius = ball_pitch_radius - ball_radius - raceway_clearance
|
|
outer_ring_inner_radius = ball_pitch_radius + ball_radius + raceway_clearance
|
|
if inner_ring_outer_radius <= bore_radius:
|
|
raise CadGenerationError(f"{component_id}: bearing balls leave no inner-ring wall thickness")
|
|
if outer_ring_inner_radius >= outer_radius:
|
|
raise CadGenerationError(f"{component_id}: bearing balls leave no outer-ring wall thickness")
|
|
chord_spacing = 2.0 * ball_pitch_radius * math.sin(math.pi / ball_count)
|
|
if chord_spacing <= params.bearing_ball_diameter_mm:
|
|
raise CadGenerationError(f"{component_id}: bearing ball_count too high for ball diameter")
|
|
if width <= params.bearing_ball_diameter_mm + 2.0 * raceway_clearance:
|
|
raise CadGenerationError(f"{component_id}: bearing width too small for rolling balls")
|
|
|
|
assembly = scad.make_assembly_rassembly(
|
|
assembly_id=f"{component_id}_radial_ball_bearing",
|
|
name=f"{component_id.replace('_', ' ').title()} radial ball bearing",
|
|
)
|
|
identity = scad.identity_placement_rplacement()
|
|
|
|
def bearing_axis_part(part_id: str, body: Any, name: str) -> Any:
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"{formula_ref}.{part_id}")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=part_id,
|
|
body=body,
|
|
name=name,
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, 0.0), f"{name} axis"),),
|
|
)
|
|
|
|
outer_ring_id = f"{component_id}_outer_ring"
|
|
inner_ring_id = f"{component_id}_inner_ring"
|
|
ball_part_id = f"{component_id}_ball"
|
|
cage_front_id = f"{component_id}_cage_front"
|
|
cage_rear_id = f"{component_id}_cage_rear"
|
|
|
|
outer_ring = _ring_solid(
|
|
scad,
|
|
outer_radius=outer_radius,
|
|
inner_radius=outer_ring_inner_radius,
|
|
height=width,
|
|
bottom_z=-width / 2.0,
|
|
)
|
|
inner_ring = _ring_solid(
|
|
scad,
|
|
outer_radius=inner_ring_outer_radius,
|
|
inner_radius=bore_radius,
|
|
height=width,
|
|
bottom_z=-width / 2.0,
|
|
)
|
|
cage_thickness = min(width * 0.10, max(0.08, params.bearing_ball_diameter_mm * 0.18))
|
|
cage_radius_half_width = max(0.04, ball_radius * 0.30)
|
|
cage_z_offset = ball_radius + raceway_clearance + cage_thickness / 2.0
|
|
cage_inner_radius = max(bore_radius, ball_pitch_radius - cage_radius_half_width)
|
|
cage_outer_radius = min(outer_radius, ball_pitch_radius + cage_radius_half_width)
|
|
if cage_z_offset + cage_thickness / 2.0 >= width / 2.0:
|
|
cage_z_offset = width / 2.0 - cage_thickness / 2.0 - raceway_clearance
|
|
cage_front = _ring_solid(
|
|
scad,
|
|
outer_radius=cage_outer_radius,
|
|
inner_radius=cage_inner_radius,
|
|
height=cage_thickness,
|
|
bottom_z=cage_z_offset - cage_thickness / 2.0,
|
|
)
|
|
cage_rear = _ring_solid(
|
|
scad,
|
|
outer_radius=cage_outer_radius,
|
|
inner_radius=cage_inner_radius,
|
|
height=cage_thickness,
|
|
bottom_z=-cage_z_offset - cage_thickness / 2.0,
|
|
)
|
|
|
|
outer_part = bearing_axis_part(outer_ring_id, outer_ring, f"{component_id} outer ring")
|
|
inner_part = bearing_axis_part(inner_ring_id, inner_ring, f"{component_id} inner ring")
|
|
cage_front_part = bearing_axis_part(cage_front_id, cage_front, f"{component_id} front cage ring")
|
|
cage_rear_part = bearing_axis_part(cage_rear_id, cage_rear, f"{component_id} rear cage ring")
|
|
ball = scad.make_sphere_rsolid(radius=ball_radius, center=(0.0, 0.0, 0.0))
|
|
ball = _tag(scad, shape=ball, instance=instance, component_id=component_id, formula_ref=f"{formula_ref}.{ball_part_id}")
|
|
ball_part = scad.make_part_rpart(part_id=ball_part_id, body=ball, name=f"{component_id} rolling ball")
|
|
ball_part = scad.assign_material_rpart(part=ball_part, material=material)
|
|
|
|
for subcomponent_id, item in [
|
|
(outer_ring_id, outer_part),
|
|
(inner_ring_id, inner_part),
|
|
(cage_front_id, cage_front_part),
|
|
(cage_rear_id, cage_rear_part),
|
|
]:
|
|
assembly = scad.add_component_rassembly(
|
|
assembly=assembly,
|
|
item=item,
|
|
component_id=subcomponent_id,
|
|
placement=identity,
|
|
name=subcomponent_id.replace("_", " ").title(),
|
|
)
|
|
|
|
ball_component_ids: list[str] = []
|
|
ball_angles: dict[str, float] = {}
|
|
digits = max(2, len(str(ball_count - 1)))
|
|
for index in range(ball_count):
|
|
angle_degrees = 360.0 * index / ball_count
|
|
angle = math.radians(angle_degrees)
|
|
ball_component_id = f"{component_id}_ball_{index:0{digits}d}"
|
|
ball_component_ids.append(ball_component_id)
|
|
ball_angles[ball_component_id] = angle_degrees
|
|
assembly = scad.add_component_rassembly(
|
|
assembly=assembly,
|
|
item=ball_part,
|
|
component_id=ball_component_id,
|
|
placement=scad.make_placement_rplacement(
|
|
origin=(
|
|
ball_pitch_radius * math.cos(angle),
|
|
ball_pitch_radius * math.sin(angle),
|
|
0.0,
|
|
),
|
|
x_axis=(math.cos(angle), math.sin(angle), 0.0),
|
|
y_axis=(-math.sin(angle), math.cos(angle), 0.0),
|
|
),
|
|
name=f"{component_id} ball {index}",
|
|
)
|
|
|
|
assembly = scad.add_revolute_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"{component_id}_inner_outer_revolute",
|
|
connector_a=scad.make_connector_ref_rconnectorref(outer_ring_id, "axis"),
|
|
connector_b=scad.make_connector_ref_rconnectorref(inner_ring_id, "axis"),
|
|
drive_angle_degrees=None,
|
|
angle_limit=None,
|
|
name=f"{component_id} inner ring spins in outer ring",
|
|
)
|
|
assembly = add_forwarded_connector(
|
|
scad,
|
|
assembly=assembly,
|
|
connector_id="outer_axis",
|
|
source_component_id=outer_ring_id,
|
|
source_connector_id="axis",
|
|
name=f"{component_id} outer ring public axis",
|
|
)
|
|
assembly = add_forwarded_connector(
|
|
scad,
|
|
assembly=assembly,
|
|
connector_id="inner_axis",
|
|
source_component_id=inner_ring_id,
|
|
source_connector_id="axis",
|
|
name=f"{component_id} inner ring public axis",
|
|
)
|
|
assembly.set_metadata(
|
|
"planetary_reducer.ball_bearing",
|
|
{
|
|
"component_id": component_id,
|
|
"bore_diameter_mm": bore_diameter,
|
|
"outer_diameter_mm": outer_diameter,
|
|
"width_mm": width,
|
|
"ball_diameter_mm": params.bearing_ball_diameter_mm,
|
|
"ball_count": ball_count,
|
|
"ball_pitch_radius_mm": ball_pitch_radius,
|
|
"raceway_clearance_mm": raceway_clearance,
|
|
"outer_ring_component_id": outer_ring_id,
|
|
"inner_ring_component_id": inner_ring_id,
|
|
"cage_component_ids": [cage_front_id, cage_rear_id],
|
|
"ball_component_ids": ball_component_ids,
|
|
"ball_angles_degrees": ball_angles,
|
|
"local_center_z_mm": center_z,
|
|
},
|
|
)
|
|
return assembly
|
|
|
|
|
|
def make_spacer(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
index: int,
|
|
material: Any,
|
|
) -> Any:
|
|
center_z = -params.axial_clearance_mm / 2.0
|
|
component_id = f"planet_spacer_{index}"
|
|
body = _ring_solid(
|
|
scad,
|
|
outer_radius=params.planet_bearing_outer_diameter_mm / 2.0,
|
|
inner_radius=params.planet_pin_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
|
|
height=params.planet_spacer_thickness_mm,
|
|
bottom_z=center_z - params.planet_spacer_thickness_mm / 2.0,
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=f"Planet spacer/retainer {index}",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Planet spacer {index} axis"),),
|
|
)
|
|
|
|
|
|
def make_shaft(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
component_id: str,
|
|
radius: float,
|
|
bottom_z: float,
|
|
height: float,
|
|
material: Any,
|
|
formula_ref: str,
|
|
) -> Any:
|
|
body = scad.make_cylinder_rsolid(
|
|
radius=radius,
|
|
height=height,
|
|
bottom_face_center=(0.0, 0.0, bottom_z),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=formula_ref)
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=component_id.replace("_", " ").title(),
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"{component_id} axis"),),
|
|
)
|
|
|
|
|
|
def make_housing_shell(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
material: Any,
|
|
) -> Any:
|
|
bottom_z = -params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm
|
|
top_z = instance.parameters.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm
|
|
body = _ring_solid(
|
|
scad,
|
|
outer_radius=params.housing_outer_diameter_mm / 2.0,
|
|
inner_radius=params.housing_inner_diameter_mm / 2.0,
|
|
height=top_z - bottom_z,
|
|
bottom_z=bottom_z,
|
|
)
|
|
body = _cut_bolt_holes(scad, shape=body, params=params, bottom_z=bottom_z, height=top_z - bottom_z)
|
|
body = _tag(scad, shape=body, instance=instance, component_id="housing", formula_ref="industrial.housing_body")
|
|
connector_specs = [
|
|
("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Housing axis"),
|
|
("front_cover_axis", (0.0, 0.0, top_z + params.cover_thickness_mm / 2.0), "Front cover axis"),
|
|
("rear_cover_axis", (0.0, 0.0, bottom_z - params.cover_thickness_mm / 2.0), "Rear cover axis"),
|
|
("ring_mount_axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), "Ring mount axis"),
|
|
("output_bearing_1_axis", (0.0, 0.0, bottom_z + params.cover_thickness_mm * 0.75), "Output bearing 1 axis"),
|
|
("output_bearing_2_axis", (0.0, 0.0, bottom_z + params.cover_thickness_mm * 1.75), "Output bearing 2 axis"),
|
|
("seal_input_axis", (0.0, 0.0, top_z + params.cover_thickness_mm * 0.75), "Input seal axis"),
|
|
("seal_output_axis", (0.0, 0.0, bottom_z - params.cover_thickness_mm * 0.75), "Output seal axis"),
|
|
]
|
|
connector_specs.extend(
|
|
(
|
|
f"bolt_{index + 1}_axis",
|
|
(x, y, instance.parameters.face_width_mm / 2.0),
|
|
f"Housing bolt {index + 1} axis",
|
|
)
|
|
for index, (x, y) in enumerate(bolt_xy_positions(params))
|
|
)
|
|
return make_axis_part(
|
|
scad,
|
|
part_id="housing",
|
|
body=body,
|
|
name="Industrial housing shell with bolt holes",
|
|
material=material,
|
|
connector_specs=tuple(connector_specs),
|
|
)
|
|
|
|
|
|
def make_cover(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
component_id: str,
|
|
center_z: float,
|
|
shaft_clearance_radius: float,
|
|
material: Any,
|
|
) -> Any:
|
|
body = _ring_solid(
|
|
scad,
|
|
outer_radius=params.housing_outer_diameter_mm / 2.0,
|
|
inner_radius=shaft_clearance_radius,
|
|
height=params.cover_thickness_mm,
|
|
bottom_z=center_z - params.cover_thickness_mm / 2.0,
|
|
)
|
|
body = _cut_bolt_holes(
|
|
scad,
|
|
shape=body,
|
|
params=params,
|
|
bottom_z=center_z - params.cover_thickness_mm / 2.0,
|
|
height=params.cover_thickness_mm,
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=component_id.replace("_", " ").title(),
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, center_z), f"{component_id} axis"),),
|
|
)
|
|
|
|
|
|
def make_bolt(
|
|
scad: Any,
|
|
*,
|
|
instance: ReducerInstance,
|
|
params: IndustrialDesignParameters,
|
|
index: int,
|
|
material: Any,
|
|
) -> Any:
|
|
component_id = f"housing_bolt_{index}"
|
|
height = params.cover_thickness_mm * 2.0
|
|
body = scad.make_cylinder_rsolid(
|
|
radius=params.bolt_diameter_mm / 2.0,
|
|
height=height,
|
|
bottom_face_center=(0.0, 0.0, -height / 2.0),
|
|
axis=(0.0, 0.0, 1.0),
|
|
)
|
|
body = _tag(scad, shape=body, instance=instance, component_id=component_id, formula_ref=f"industrial.{component_id}")
|
|
return make_axis_part(
|
|
scad,
|
|
part_id=component_id,
|
|
body=body,
|
|
name=f"Housing bolt {index}",
|
|
material=material,
|
|
connector_specs=(("axis", (0.0, 0.0, instance.parameters.face_width_mm / 2.0), f"Housing bolt {index} axis"),),
|
|
)
|
|
|
|
|
|
def make_industrial_parts(scad: Any, *, instance: ReducerInstance) -> dict[str, Any]:
|
|
if instance.industrial_parameters is None:
|
|
raise CadGenerationError("industrial_core_missing_industrial_parameters")
|
|
params = instance.industrial_parameters
|
|
p = instance.parameters
|
|
materials = make_materials(scad)
|
|
shaft_material = materials["gear"]
|
|
bearing_material = materials["ring"]
|
|
output_shaft_extension = max(12.0, p.face_width_mm * 1.50)
|
|
parts: dict[str, Any] = {
|
|
"sun": make_industrial_sun(scad, instance=instance, params=params, material=materials["gear"]),
|
|
"planet": make_planet(scad, instance=instance, material=materials["gear"]),
|
|
"ring": make_ring(scad, instance=instance, material=materials["ring"]),
|
|
"carrier": make_industrial_carrier_body(scad, instance=instance, params=params, material=materials["carrier"]),
|
|
"carrier_front_plate": make_carrier_front_plate(scad, instance=instance, params=params, material=materials["carrier"]),
|
|
"housing": make_housing_shell(scad, instance=instance, params=params, material=materials["housing"]),
|
|
"sun_input_key": make_input_key(scad, instance=instance, params=params, material=shaft_material),
|
|
"sun_input_shaft": make_shaft(
|
|
scad,
|
|
instance=instance,
|
|
component_id="sun_input_shaft",
|
|
radius=params.input_shaft_diameter_mm / 2.0,
|
|
bottom_z=-params.axial_clearance_mm,
|
|
height=(
|
|
p.face_width_mm
|
|
+ params.carrier_front_plate_thickness_mm
|
|
+ 3.0 * params.axial_clearance_mm
|
|
+ 3.0 * params.cover_thickness_mm
|
|
),
|
|
material=shaft_material,
|
|
formula_ref="industrial.sun_input_shaft",
|
|
),
|
|
"output_shaft": make_shaft(
|
|
scad,
|
|
instance=instance,
|
|
component_id="output_shaft",
|
|
radius=params.output_shaft_diameter_mm / 2.0,
|
|
bottom_z=(
|
|
-params.carrier_plate_thickness_mm
|
|
- 3.0 * params.cover_thickness_mm
|
|
- 2.0 * params.axial_clearance_mm
|
|
- output_shaft_extension
|
|
),
|
|
height=(
|
|
params.carrier_plate_thickness_mm
|
|
+ 3.0 * params.cover_thickness_mm
|
|
+ params.axial_clearance_mm
|
|
+ output_shaft_extension
|
|
),
|
|
material=shaft_material,
|
|
formula_ref="industrial.output_shaft",
|
|
),
|
|
"output_bearing_1": make_ball_bearing_part(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="output_bearing_1",
|
|
bore_diameter=params.output_bearing_inner_diameter_mm,
|
|
outer_diameter=params.output_bearing_outer_diameter_mm,
|
|
width=max(0.8, params.cover_thickness_mm * 0.7),
|
|
center_z=-params.carrier_plate_thickness_mm - params.axial_clearance_mm - params.cover_thickness_mm * 0.35,
|
|
ball_count=params.output_bearing_ball_count,
|
|
material=bearing_material,
|
|
formula_ref="industrial.output_bearing_1",
|
|
),
|
|
"output_bearing_2": make_ball_bearing_part(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="output_bearing_2",
|
|
bore_diameter=params.output_bearing_inner_diameter_mm,
|
|
outer_diameter=params.output_bearing_outer_diameter_mm,
|
|
width=max(0.8, params.cover_thickness_mm * 0.7),
|
|
center_z=-params.carrier_plate_thickness_mm - params.axial_clearance_mm - params.cover_thickness_mm * 1.35,
|
|
ball_count=params.output_bearing_ball_count,
|
|
material=bearing_material,
|
|
formula_ref="industrial.output_bearing_2",
|
|
),
|
|
"ring_mount": make_bearing_ring(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="ring_mount",
|
|
inner_diameter=instance.derived.ring_outer_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
|
|
outer_diameter=params.housing_inner_diameter_mm - 2.0 * params.bearing_seat_clearance_mm,
|
|
width=max(0.8, p.face_width_mm * 0.18),
|
|
center_z=p.face_width_mm / 2.0,
|
|
material=materials["housing"],
|
|
formula_ref="industrial.ring_mount",
|
|
),
|
|
"seal_input": make_bearing_ring(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="seal_input",
|
|
inner_diameter=params.input_shaft_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
|
|
outer_diameter=params.input_shaft_diameter_mm + 2.0 * max(params.radial_clearance_mm, params.bearing_seat_clearance_mm * 2.0),
|
|
width=max(0.5, p.module_mm),
|
|
center_z=p.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm,
|
|
material=materials["housing"],
|
|
formula_ref="industrial.seal_input",
|
|
),
|
|
"seal_output": make_bearing_ring(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="seal_output",
|
|
inner_diameter=params.output_shaft_diameter_mm + 2.0 * params.bearing_seat_clearance_mm,
|
|
outer_diameter=params.output_bearing_outer_diameter_mm,
|
|
width=max(0.5, p.module_mm),
|
|
center_z=-params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm,
|
|
material=materials["housing"],
|
|
formula_ref="industrial.seal_output",
|
|
),
|
|
}
|
|
housing_bottom = -params.carrier_plate_thickness_mm - params.cover_thickness_mm - 2.0 * params.axial_clearance_mm
|
|
housing_top = p.face_width_mm + params.carrier_front_plate_thickness_mm + params.cover_thickness_mm + 2.0 * params.axial_clearance_mm
|
|
parts["front_cover"] = make_cover(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="front_cover",
|
|
center_z=housing_top + params.cover_thickness_mm / 2.0,
|
|
shaft_clearance_radius=params.input_shaft_diameter_mm / 2.0 + params.radial_clearance_mm,
|
|
material=materials["housing"],
|
|
)
|
|
parts["rear_cover"] = make_cover(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id="rear_cover",
|
|
center_z=housing_bottom - params.cover_thickness_mm / 2.0,
|
|
shaft_clearance_radius=params.output_bearing_outer_diameter_mm / 2.0 + params.bearing_seat_clearance_mm,
|
|
material=materials["housing"],
|
|
)
|
|
for index in range(1, p.planet_count + 1):
|
|
parts[f"planet_pin_{index}"] = make_pin(scad, instance=instance, params=params, index=index, material=shaft_material)
|
|
parts[f"planet_bearing_{index}"] = make_ball_bearing_part(
|
|
scad,
|
|
instance=instance,
|
|
params=params,
|
|
component_id=f"planet_bearing_{index}",
|
|
bore_diameter=params.planet_bearing_inner_diameter_mm,
|
|
outer_diameter=params.planet_bearing_outer_diameter_mm,
|
|
width=params.planet_bearing_width_mm,
|
|
center_z=p.face_width_mm / 2.0,
|
|
ball_count=params.planet_bearing_ball_count,
|
|
material=bearing_material,
|
|
formula_ref=f"industrial.planet_bearing_{index}",
|
|
)
|
|
parts[f"planet_spacer_{index}"] = make_spacer(scad, instance=instance, params=params, index=index, material=materials["carrier"])
|
|
for index in range(1, params.bolt_count + 1):
|
|
parts[f"housing_bolt_{index}"] = make_bolt(scad, instance=instance, params=params, index=index, material=shaft_material)
|
|
return parts
|
|
|
|
|
|
def _add_component(
|
|
scad: Any,
|
|
*,
|
|
assembly: Any,
|
|
parts: dict[str, Any],
|
|
component_id: str,
|
|
placement: Any,
|
|
name: str,
|
|
) -> Any:
|
|
return scad.add_component_rassembly(
|
|
assembly=assembly,
|
|
item=parts[component_id],
|
|
component_id=component_id,
|
|
placement=placement,
|
|
name=name,
|
|
)
|
|
|
|
|
|
def build_industrial_assembly(scad: Any, *, instance: ReducerInstance, parts: dict[str, Any]) -> Any:
|
|
require_assembly_api(scad)
|
|
p = instance.parameters
|
|
d = instance.derived
|
|
params = instance.industrial_parameters
|
|
if params is None:
|
|
raise CadGenerationError("industrial_core_missing_industrial_parameters")
|
|
fixed_member = instance.boundary.fixed
|
|
if fixed_member is None:
|
|
raise CadGenerationError("industrial_core_requires_fixed_member")
|
|
placement_plan = solve_reducer_placements(instance)
|
|
|
|
def placement_for(component_id: str) -> Any:
|
|
pose = placement_plan.require_pose(component_id)
|
|
return make_z_rotation_placement(
|
|
scad,
|
|
origin=pose.translation_mm,
|
|
angle_degrees=pose.rotation_axis_angle_deg[3],
|
|
)
|
|
|
|
assembly = scad.make_assembly_rassembly(
|
|
assembly_id=f"{instance.run_id}_simple_2kh_industrial_{p.tooth_form}_reducer",
|
|
name=f"Formula-generated industrial-core simple 2K-H {p.tooth_form} planetary reducer",
|
|
)
|
|
identity = scad.identity_placement_rplacement()
|
|
for component_id in [
|
|
"housing",
|
|
"ring",
|
|
"sun",
|
|
"carrier",
|
|
"carrier_front_plate",
|
|
"sun_input_shaft",
|
|
"sun_input_key",
|
|
"output_shaft",
|
|
"front_cover",
|
|
"rear_cover",
|
|
"ring_mount",
|
|
"seal_input",
|
|
"seal_output",
|
|
]:
|
|
assembly = _add_component(
|
|
scad,
|
|
assembly=assembly,
|
|
parts=parts,
|
|
component_id=component_id,
|
|
placement=placement_for(component_id),
|
|
name=component_id.replace("_", " ").title(),
|
|
)
|
|
for component_id in ["output_bearing_1", "output_bearing_2"]:
|
|
assembly = _add_component(
|
|
scad,
|
|
assembly=assembly,
|
|
parts=parts,
|
|
component_id=component_id,
|
|
placement=placement_for(component_id),
|
|
name=component_id.replace("_", " ").title(),
|
|
)
|
|
for index in range(p.planet_count):
|
|
planet_component = f"planet_{index + 1}"
|
|
assembly = scad.add_component_rassembly(
|
|
assembly=assembly,
|
|
item=parts["planet"],
|
|
component_id=planet_component,
|
|
placement=placement_for(planet_component),
|
|
name=f"Planet gear {index + 1}",
|
|
)
|
|
for local in ["pin", "spacer"]:
|
|
component_id = f"planet_{local}_{index + 1}"
|
|
assembly = _add_component(
|
|
scad,
|
|
assembly=assembly,
|
|
parts=parts,
|
|
component_id=component_id,
|
|
placement=placement_for(component_id),
|
|
name=component_id.replace("_", " ").title(),
|
|
)
|
|
bearing_id = f"planet_bearing_{index + 1}"
|
|
assembly = _add_component(
|
|
scad,
|
|
assembly=assembly,
|
|
parts=parts,
|
|
component_id=bearing_id,
|
|
placement=placement_for(bearing_id),
|
|
name=bearing_id.replace("_", " ").title(),
|
|
)
|
|
for index in range(1, params.bolt_count + 1):
|
|
assembly = _add_component(
|
|
scad,
|
|
assembly=assembly,
|
|
parts=parts,
|
|
component_id=f"housing_bolt_{index}",
|
|
placement=placement_for(f"housing_bolt_{index}"),
|
|
name=f"Housing bolt {index}",
|
|
)
|
|
|
|
assembly = scad.ground_component_rassembly(assembly=assembly, component_id=fixed_member)
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"housing_fixed_to_{fixed_member}",
|
|
connector_a=cref(scad, component_id=fixed_member, connector_id="axis"),
|
|
connector_b=cref(scad, component_id="housing", connector_id="axis"),
|
|
name=f"Housing fixed to {fixed_member}",
|
|
)
|
|
for member in ("sun", "ring", "carrier"):
|
|
if member == fixed_member:
|
|
continue
|
|
role = role_for_component(instance, member)
|
|
assembly = scad.add_revolute_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"{member}_{role}_revolute",
|
|
connector_a=cref(scad, component_id=fixed_member, connector_id="axis"),
|
|
connector_b=cref(scad, component_id=member, connector_id="axis"),
|
|
drive_angle_degrees=0.0 if role == "input" else None,
|
|
angle_limit=None,
|
|
name=f"{member.title()} {role} revolute",
|
|
)
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id="sun_input_shaft_keyed_to_sun",
|
|
connector_a=cref(scad, component_id="sun", connector_id="axis"),
|
|
connector_b=cref(scad, component_id="sun_input_shaft", connector_id="axis"),
|
|
name="Sun input shaft keyed to sun gear",
|
|
)
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id="sun_input_key_fixed_to_sun_input_shaft",
|
|
connector_a=cref(scad, component_id="sun_input_shaft", connector_id="axis"),
|
|
connector_b=cref(scad, component_id="sun_input_key", connector_id="axis"),
|
|
name="Input key fixed to sun input shaft",
|
|
)
|
|
for constraint_id, component_id, housing_connector in [
|
|
("carrier_front_plate_rigid_to_carrier", "carrier_front_plate", "axis"),
|
|
("carrier_body_rigid_to_output_shaft", "output_shaft", "axis"),
|
|
("output_bearing_1_fixed_to_housing", "output_bearing_1", "output_bearing_1_axis"),
|
|
("output_bearing_2_fixed_to_housing", "output_bearing_2", "output_bearing_2_axis"),
|
|
("front_cover_bolted_to_housing", "front_cover", "front_cover_axis"),
|
|
("rear_cover_bolted_to_housing", "rear_cover", "rear_cover_axis"),
|
|
("ring_mount_bolted_to_housing", "ring_mount", "ring_mount_axis"),
|
|
("seal_input_fixed_to_housing", "seal_input", "seal_input_axis"),
|
|
("seal_output_fixed_to_housing", "seal_output", "seal_output_axis"),
|
|
]:
|
|
base_component = "carrier" if component_id in {"carrier_front_plate", "output_shaft"} else "housing"
|
|
base_connector = "axis" if base_component == "carrier" else housing_connector
|
|
child_connector = "outer_axis" if component_id.startswith("output_bearing_") else "axis"
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=constraint_id,
|
|
connector_a=cref(scad, component_id=base_component, connector_id=base_connector),
|
|
connector_b=cref(scad, component_id=component_id, connector_id=child_connector),
|
|
name=constraint_id.replace("_", " ").title(),
|
|
)
|
|
|
|
for index in range(p.planet_count):
|
|
planet_id = f"planet_{index + 1}"
|
|
pin_id = f"planet_pin_{index + 1}"
|
|
bearing_id = f"planet_bearing_{index + 1}"
|
|
spacer_id = f"planet_spacer_{index + 1}"
|
|
carrier_connector = f"planet_{index + 1}_axis"
|
|
assembly = scad.add_revolute_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"{planet_id}_pin_revolute",
|
|
connector_a=cref(scad, component_id="carrier", connector_id=carrier_connector),
|
|
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
|
|
drive_angle_degrees=None,
|
|
angle_limit=None,
|
|
name=f"Planet {index + 1} bearing revolute",
|
|
)
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"{pin_id}_press_fit_to_carrier",
|
|
connector_a=cref(scad, component_id="carrier", connector_id=carrier_connector),
|
|
connector_b=cref(scad, component_id=pin_id, connector_id="axis"),
|
|
name=f"Planet pin {index + 1} press fit to carrier",
|
|
)
|
|
for child_id, label in [(bearing_id, "bearing"), (spacer_id, "spacer")]:
|
|
child_connector = "inner_axis" if child_id == bearing_id else "axis"
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"{child_id}_fixed_to_{pin_id}",
|
|
connector_a=cref(scad, component_id=pin_id, connector_id="axis"),
|
|
connector_b=cref(scad, component_id=child_id, connector_id=child_connector),
|
|
name=f"Planet {index + 1} {label} fixed to pin reference",
|
|
)
|
|
assembly = scad.add_gear_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"sun_to_{planet_id}_external_mesh",
|
|
connector_a=cref(scad, component_id="sun", connector_id="axis"),
|
|
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
|
|
pitch_radius_a=d.sun_pitch_radius_mm,
|
|
pitch_radius_b=d.planet_pitch_radius_mm,
|
|
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_id}_internal_mesh",
|
|
connector_a=cref(scad, component_id="ring", connector_id="axis"),
|
|
connector_b=cref(scad, component_id=planet_id, connector_id="axis"),
|
|
pulley_radius_a=d.ring_pitch_radius_mm,
|
|
pulley_radius_b=d.planet_pitch_radius_mm,
|
|
phase_offset=None,
|
|
name=f"Ring internal mesh to planet {index + 1}",
|
|
)
|
|
for index in range(1, params.bolt_count + 1):
|
|
assembly = scad.add_fixed_constraint_rassembly(
|
|
assembly=assembly,
|
|
constraint_id=f"housing_bolt_{index}_fixed_to_housing",
|
|
connector_a=cref(scad, component_id="housing", connector_id=f"bolt_{index}_axis"),
|
|
connector_b=cref(scad, component_id=f"housing_bolt_{index}", connector_id="axis"),
|
|
name=f"Housing bolt {index} fixed to housing",
|
|
)
|
|
return scad.solve_assembly_constraints_rassembly(assembly=assembly, strict=True)
|
|
|
|
|
|
def _component_center(instance: ReducerInstance, component_id: str) -> tuple[float, float, float]:
|
|
return solve_reducer_placements(instance).require_pose(component_id).center_xyz_mm
|
|
|
|
|
|
def _industrial_component_manifests(
|
|
instance: ReducerInstance,
|
|
*,
|
|
placement_plan: PlacementPlan,
|
|
component_step_paths: dict[str, str] | None = None,
|
|
) -> dict[str, ComponentManifest]:
|
|
if instance.assembly_topology is None or instance.industrial_parameters is None:
|
|
raise CadGenerationError("industrial_core_missing_assembly_topology")
|
|
p = instance.parameters
|
|
d = instance.derived
|
|
params = instance.industrial_parameters
|
|
component_step_paths = component_step_paths or {}
|
|
sun_spec = gear_spec_for_component(instance, "sun")
|
|
planet_spec = gear_spec_for_component(instance, "planet")
|
|
ring_spec = gear_spec_for_component(instance, "ring")
|
|
physical_by_id = {component.component_id: component for component in instance.assembly_topology.components}
|
|
|
|
components: dict[str, ComponentManifest] = {}
|
|
for component_id, physical in physical_by_id.items():
|
|
gear_type = None
|
|
tooth_count = None
|
|
pitch_radius = None
|
|
tooth_form = None
|
|
helix_angle = None
|
|
helix_hand = None
|
|
module = None
|
|
pressure_angle = None
|
|
face_width = None
|
|
if component_id == "sun":
|
|
gear_type = "external"
|
|
tooth_count = p.z_sun
|
|
pitch_radius = d.sun_pitch_radius_mm
|
|
spec = sun_spec
|
|
elif component_id == "ring":
|
|
gear_type = "internal"
|
|
tooth_count = p.z_ring
|
|
pitch_radius = d.ring_pitch_radius_mm
|
|
spec = ring_spec
|
|
elif component_id.startswith("planet_") and component_id.split("_")[-1].isdigit() and physical.kind == "gear":
|
|
gear_type = "external"
|
|
tooth_count = p.z_planet
|
|
pitch_radius = d.planet_pitch_radius_mm
|
|
spec = planet_spec
|
|
else:
|
|
spec = None
|
|
if spec is not None:
|
|
tooth_form = p.tooth_form
|
|
helix_angle = spec.helix_angle_deg
|
|
helix_hand = spec.helix_hand
|
|
module = p.module_mm
|
|
pressure_angle = p.pressure_angle_deg
|
|
face_width = p.face_width_mm
|
|
role = role_for_component(instance, component_id) if component_id in {"sun", "ring", "carrier"} else physical.role
|
|
tags = [
|
|
f"formula.run_id.{instance.run_id}",
|
|
f"formula.node.{component_id}",
|
|
f"formula.ref.{physical.formula_ref}",
|
|
]
|
|
if role in {"input", "fixed", "output"}:
|
|
tags.append(f"formula.role.{role}")
|
|
details: dict[str, Any] = {}
|
|
if physical.kind == "bearing":
|
|
if component_id.startswith("planet_bearing_"):
|
|
details = {
|
|
"bearing_type": "radial_ball_bearing",
|
|
"standard_series": "parametric_deep_groove",
|
|
"inner_diameter_mm": params.planet_bearing_inner_diameter_mm,
|
|
"outer_diameter_mm": params.planet_bearing_outer_diameter_mm,
|
|
"width_mm": params.planet_bearing_width_mm,
|
|
"ball_diameter_mm": params.bearing_ball_diameter_mm,
|
|
"ball_count": params.planet_bearing_ball_count,
|
|
}
|
|
elif component_id.startswith("output_bearing_"):
|
|
details = {
|
|
"bearing_type": "radial_ball_bearing",
|
|
"standard_series": "parametric_deep_groove",
|
|
"inner_diameter_mm": params.output_bearing_inner_diameter_mm,
|
|
"outer_diameter_mm": params.output_bearing_outer_diameter_mm,
|
|
"width_mm": max(0.8, params.cover_thickness_mm * 0.7),
|
|
"ball_diameter_mm": params.bearing_ball_diameter_mm,
|
|
"ball_count": params.output_bearing_ball_count,
|
|
}
|
|
elif component_id == "sun_input_key":
|
|
details = {
|
|
"coupling_type": "parallel_key",
|
|
"width_mm": params.sun_key_width_mm,
|
|
"height_mm": params.sun_key_height_mm,
|
|
"length_mm": params.sun_key_length_mm,
|
|
"mates_with": ["sun_keyway", "sun_input_shaft_keyseat"],
|
|
}
|
|
components[component_id] = ComponentManifest(
|
|
component_id=component_id,
|
|
formula_node=physical.formula_ref,
|
|
role=role,
|
|
kind=physical.kind,
|
|
gear_type=gear_type,
|
|
tooth_count=tooth_count,
|
|
tooth_form=tooth_form,
|
|
helix_angle_deg=helix_angle,
|
|
helix_hand=helix_hand,
|
|
module_mm=module,
|
|
pressure_angle_deg=pressure_angle,
|
|
face_width_mm=face_width,
|
|
pitch_radius_mm=pitch_radius,
|
|
center_xyz_mm=placement_plan.require_pose(component_id).center_xyz_mm,
|
|
axis_xyz=placement_plan.require_pose(component_id).axis_xyz,
|
|
tags=tags,
|
|
assembled_step_path=component_step_paths.get(component_id),
|
|
details=details,
|
|
)
|
|
return components
|
|
|
|
|
|
def _industrial_constraint_manifests(instance: ReducerInstance) -> list[ConstraintManifest]:
|
|
p = instance.parameters
|
|
params = instance.industrial_parameters
|
|
if params is None or instance.assembly_topology is None:
|
|
raise CadGenerationError("industrial_core_missing_assembly_topology")
|
|
fixed_member = instance.boundary.fixed
|
|
if fixed_member is None:
|
|
raise CadGenerationError("industrial_core_requires_fixed_member")
|
|
constraints: list[ConstraintManifest] = [
|
|
ConstraintManifest(
|
|
constraint_id=f"housing_fixed_to_{fixed_member}",
|
|
relation_type="fixed",
|
|
component_a=fixed_member,
|
|
component_b="housing",
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="fixed",
|
|
)
|
|
]
|
|
for member in ("sun", "ring", "carrier"):
|
|
if member == fixed_member:
|
|
continue
|
|
role = role_for_component(instance, member)
|
|
constraints.append(
|
|
ConstraintManifest(
|
|
constraint_id=f"{member}_{role}_revolute",
|
|
relation_type="revolute_joint",
|
|
component_a=fixed_member,
|
|
component_b=member,
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="revolute",
|
|
)
|
|
)
|
|
constraints.extend(
|
|
[
|
|
ConstraintManifest(constraint_id="sun_input_shaft_keyed_to_sun", relation_type="keyed", component_a="sun_input_shaft", component_b="sun", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="sun_input_key_fixed_to_sun_input_shaft", relation_type="rigid", component_a="sun_input_key", component_b="sun_input_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="carrier_body_rigid_to_output_shaft", relation_type="rigid", component_a="carrier", component_b="output_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="carrier_front_plate_rigid_to_carrier", relation_type="rigid", component_a="carrier_front_plate", component_b="carrier", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="output_bearing_1_fixed_to_housing", relation_type="bearing_support", component_a="output_bearing_1", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="output_bearing_2_fixed_to_housing", relation_type="bearing_support", component_a="output_bearing_2", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="front_cover_bolted_to_housing", relation_type="bolted", component_a="front_cover", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="rear_cover_bolted_to_housing", relation_type="bolted", component_a="rear_cover", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="ring_mount_bolted_to_housing", relation_type="bolted", component_a="ring_mount", component_b="housing", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="seal_input_fixed_to_housing", relation_type="coaxial", component_a="seal_input", component_b="sun_input_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id="seal_output_fixed_to_housing", relation_type="coaxial", component_a="seal_output", component_b="output_shaft", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
]
|
|
)
|
|
for index in range(1, p.planet_count + 1):
|
|
planet_id = f"planet_{index}"
|
|
pin_id = f"planet_pin_{index}"
|
|
bearing_id = f"planet_bearing_{index}"
|
|
spacer_id = f"planet_spacer_{index}"
|
|
constraints.extend(
|
|
[
|
|
ConstraintManifest(constraint_id=f"{planet_id}_pin_revolute", relation_type="revolute_joint", component_a="carrier", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="revolute"),
|
|
ConstraintManifest(constraint_id=f"{pin_id}_press_fit_to_carrier", relation_type="press_fit", component_a=pin_id, component_b="carrier", source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id=f"{bearing_id}_fixed_to_{pin_id}", relation_type="slip_fit", component_a=bearing_id, component_b=pin_id, source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id=f"{spacer_id}_fixed_to_{pin_id}", relation_type="axial_retention", component_a=spacer_id, component_b=pin_id, source="simplecadapi_constraint", cad_constraint_kind="fixed"),
|
|
ConstraintManifest(constraint_id=f"sun_to_{planet_id}_external_mesh", relation_type="external_mesh", component_a="sun", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="gear"),
|
|
ConstraintManifest(constraint_id=f"ring_to_{planet_id}_internal_mesh", relation_type="internal_mesh", component_a="ring", component_b=planet_id, source="simplecadapi_constraint", cad_constraint_kind="belt"),
|
|
]
|
|
)
|
|
for relation in instance.assembly_topology.relations:
|
|
if not any(existing.constraint_id == relation.relation_id for existing in constraints):
|
|
constraints.append(
|
|
ConstraintManifest(
|
|
constraint_id=relation.relation_id,
|
|
relation_type=relation.relation_type,
|
|
component_a=relation.source_component_id,
|
|
component_b=relation.target_component_id,
|
|
source="formula_semantic",
|
|
cad_constraint_kind=None,
|
|
relation_id=relation.relation_id,
|
|
)
|
|
)
|
|
for index in range(1, params.bolt_count + 1):
|
|
constraints.append(
|
|
ConstraintManifest(
|
|
constraint_id=f"housing_bolt_{index}_fixed_to_housing",
|
|
relation_type="bolted",
|
|
component_a=f"housing_bolt_{index}",
|
|
component_b="housing",
|
|
source="simplecadapi_constraint",
|
|
cad_constraint_kind="fixed",
|
|
)
|
|
)
|
|
return constraints
|
|
|
|
|
|
def generate_simplecad_industrial(instance: ReducerInstance, out_dir: Path) -> AssemblyManifest:
|
|
if instance.topology_family != "simple_2k_h":
|
|
raise CadGenerationError("industrial_core supports only simple_2k_h in this implementation")
|
|
if instance.parameters.tooth_form not in {"spur", "helical"}:
|
|
raise CadGenerationError(f"unsupported_tooth_form: {instance.parameters.tooth_form}")
|
|
if instance.industrial_parameters is None:
|
|
instance = instance.model_copy(
|
|
update={
|
|
"industrial_parameters": compute_industrial_parameters(
|
|
parameters=instance.parameters,
|
|
derived=instance.derived,
|
|
)
|
|
}
|
|
)
|
|
if instance.assembly_topology is None:
|
|
instance = instance.model_copy(update={"assembly_topology": build_industrial_assembly_topology(instance)})
|
|
|
|
scad = import_simplecadapi()
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
step_path = out_dir / "reducer.step"
|
|
model_path = out_dir / "reducer.model.json"
|
|
session_path = out_dir / "reducer.session.json"
|
|
manifest_path = out_dir / "assembly_manifest.json"
|
|
build_meta_path = out_dir / "build_meta.json"
|
|
industrial_parameters_path = out_dir / "industrial_parameters.json"
|
|
assembly_topology_path = out_dir / "assembly_topology.json"
|
|
placement_plan_path = out_dir / "placement_plan.json"
|
|
placement_plan = solve_reducer_placements(instance)
|
|
|
|
graph_id = f"{instance.run_id}_simple_2kh_industrial_{instance.parameters.tooth_form}_generation"
|
|
with scad.GraphSession(graph_id=graph_id) as session:
|
|
parts = make_industrial_parts(scad, instance=instance)
|
|
assembly = build_industrial_assembly(scad, instance=instance, parts=parts)
|
|
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
|
component_step_paths = _export_assembled_component_steps(scad, assembly=assembly, out_dir=out_dir)
|
|
model_json, session_json = export_model_session_json(scad, session=session)
|
|
|
|
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))
|
|
write_model_json(industrial_parameters_path, instance.industrial_parameters)
|
|
write_model_json(assembly_topology_path, instance.assembly_topology)
|
|
write_model_json(placement_plan_path, placement_plan)
|
|
|
|
model_payload = json.loads(model_json)
|
|
preview_solids = len(preview.get_solids())
|
|
preview_volume = float(preview.get_volume())
|
|
exported_files = {
|
|
"step": str(step_path.resolve()),
|
|
"model": str(model_path.resolve()),
|
|
"session": str(session_path.resolve()),
|
|
"manifest": str(manifest_path.resolve()),
|
|
"build_meta": str(build_meta_path.resolve()),
|
|
"component_steps_dir": str((out_dir / "component_steps").resolve()),
|
|
"industrial_parameters": str(industrial_parameters_path.resolve()),
|
|
"assembly_topology": str(assembly_topology_path.resolve()),
|
|
"placement_plan": str(placement_plan_path.resolve()),
|
|
}
|
|
manifest = AssemblyManifest(
|
|
run_id=instance.run_id,
|
|
formula_instance_hash=sha256_json(instance.model_dump(mode="json")),
|
|
generated_at=utc_now_iso(),
|
|
generator=f"simplecadapi_{instance.parameters.tooth_form}_simple_2kh_industrial_core_v0.2.0",
|
|
components=_industrial_component_manifests(instance, placement_plan=placement_plan, component_step_paths=component_step_paths),
|
|
constraints=_industrial_constraint_manifests(instance),
|
|
exported_files=exported_files,
|
|
geometry_summary={
|
|
"preview_solids": preview_solids,
|
|
"preview_volume": preview_volume,
|
|
"model_graph_nodes": len(model_payload.get("graph", {}).get("nodes", [])),
|
|
"assembled_component_step_count": len(component_step_paths),
|
|
"physical_component_count": len(instance.assembly_topology.components if instance.assembly_topology else []),
|
|
"physical_relation_count": len(instance.assembly_topology.relations if instance.assembly_topology else []),
|
|
"detail_level": instance.detail_level,
|
|
"tooth_form": instance.parameters.tooth_form,
|
|
"input_coupling": "sun_input_shaft_keyed_to_sun",
|
|
"bearing_representation": "radial_ball_bearing_with_inner_ring_outer_ring_balls_and_cage",
|
|
"internal_ring_phase_correction_deg": internal_ring_phase_correction_degrees(
|
|
z_planet=instance.parameters.z_planet,
|
|
z_ring=instance.parameters.z_ring,
|
|
),
|
|
"planet_phase_formula": "180 - 180 / z_planet + planet_center_angle_deg * (1 + z_sun / z_planet)",
|
|
"placement_solver": placement_plan.solver_version,
|
|
},
|
|
)
|
|
write_model_json(manifest_path, manifest)
|
|
build_meta = {
|
|
"run_id": instance.run_id,
|
|
"formula_instance_hash": manifest.formula_instance_hash,
|
|
"generator": manifest.generator,
|
|
"simplecadapi_file": getattr(scad, "__file__", "unknown"),
|
|
"graph_id": graph_id,
|
|
"detail_level": instance.detail_level,
|
|
"tooth_form": instance.parameters.tooth_form,
|
|
"input_coupling": "sun_input_shaft_keyed_to_sun",
|
|
"bearing_representation": "radial_ball_bearing_with_inner_ring_outer_ring_balls_and_cage",
|
|
"internal_ring_phase_correction_deg": manifest.geometry_summary["internal_ring_phase_correction_deg"],
|
|
"planet_phase_formula": "180 - 180 / z_planet + planet_center_angle_deg * (1 + z_sun / z_planet)",
|
|
"boundary": instance.boundary.model_dump(mode="json"),
|
|
"industrial_parameters": instance.industrial_parameters.model_dump(mode="json") if instance.industrial_parameters else None,
|
|
"assembly_topology_hash": sha256_json(instance.assembly_topology.model_dump(mode="json")) if instance.assembly_topology else None,
|
|
"placement_plan_hash": sha256_json(placement_plan.model_dump(mode="json")),
|
|
"assembly_id": getattr(assembly, "assembly_id", None),
|
|
"component_ids": assembly.component_ids(),
|
|
"constraint_ids": assembly.constraint_ids(),
|
|
"preview_solids": preview_solids,
|
|
"preview_volume": preview_volume,
|
|
"model_graph_nodes": manifest.geometry_summary["model_graph_nodes"],
|
|
"exported_files": exported_files,
|
|
"file_mtime_ns": {
|
|
"step": step_path.stat().st_mtime_ns,
|
|
"model": model_path.stat().st_mtime_ns,
|
|
"session": session_path.stat().st_mtime_ns,
|
|
"manifest": manifest_path.stat().st_mtime_ns,
|
|
"placement_plan": placement_plan_path.stat().st_mtime_ns,
|
|
},
|
|
}
|
|
write_json(build_meta_path, build_meta)
|
|
return manifest
|