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