# Manufacturing The manufacturing module sits between the OpenRocket design phase and cadsmith CAD generation. The **manufacturing agent** (`agents/manufacturing.md`) owns all manufacturing decisions — which components get printed, fused, purchased, or skipped — and applies DFAM rules to the component tree. ## Pipeline Position ``` openrocket_component(action="read") → gui/component_tree.json (raw) ↓ manufacturing_annotate_tree → gui/component_tree.json (annotated) ↓ ↑ ↓ feedback loop via ↓ openrocket_component ↓ (dimension changes) ↓ cadsmith reads annotated tree → STEP/STL files ``` The manufacturing agent may send dimension changes back to the OpenRocket agent (e.g., fin thickness from 3mm to 12.7mm) via `openrocket_component(action="update")`, then regenerate the tree to reflect the updated design. ## MCP Tools | Tool | Description | |------|-------------| | `manufacturing_annotate_tree` | Apply DFAM rules to an existing `component_tree.json`, annotating each component with manufacturing decisions; writes the annotated tree back to the project directory. Optional `out_path` overrides the output location. | ## ComponentTree Model The component tree is a three-level hierarchy defined in `manufacturing/models.py`: ``` ComponentTree schema_version: int source_ork: str project_root: str generated_at: str (ISO 8601) rocket_name: str stages: list[Stage] Stage name: str components: list[Component] cg, cp: QuantityField (mm) stability_cal: float max_diameter: QuantityField (mm) Component type: str # OpenRocket type (e.g. "BodyTube") name: str category: ComponentCategory # structural, recovery, hardware, electronics, propulsion dimensions: Dimensions # discriminated union of dimension models mass: QuantityField | None override_mass: QuantityField | None override_mass_enabled: bool material: str | None human_notes: str | None agent: AgentAnnotation | None # populated by manufacturing_annotate_tree cost: float | None step_path: str | None children: list[Component] ``` The tree is a living document that evolves through three phases: 1. **OpenRocket agent** populates it from the `.ork` file. 2. **Manufacturing agent** annotates it with DFAM decisions. 3. **CADSmith agent** reads it to generate CAD. Components are never removed from the tree. Fusion is an annotation, not a deletion. ## AgentAnnotation Each component's `agent` field holds structured manufacturing decisions: ```python class AgentAnnotation(BaseModel): fate: Fate | None # print, fuse, purchase, skip fused_into: str | None # name of the parent part this fuses into reason: str | None # human-readable rationale updated_by: str | None # agent name (e.g. "manufacturing") updated_at: str | None # ISO 8601 timestamp model_config = {"extra": "allow"} # allows extra DFAM fields ``` The `Fate` enum has four values: | Fate | Meaning | |------|---------| | `print` | Standalone printed part | | `fuse` | Integrated into another printed part | | `purchase` | Bought off the shelf (e.g. parachute, motor) | | `skip` | Not a physical part (structural wrapper, absorbed by fusion) | Extra fields (prefixed `dfam_`) carry AM-specific parameters like `dfam_thickness_mm`, `dfam_fillet_mm`, `dfam_shoulder_od_mm`, etc. These are allowed by the `extra = "allow"` model config. ## DFAM Rules The `annotate_dfam()` function in `manufacturing/dfam.py` walks the component tree and applies these rules: | Component Type | Default Fate | Rule | |----------------|-------------|------| | `NoseCone` | `print` | Standalone part; integral shoulder added with configurable length (default 30 mm) and OD matched to body tube ID | | `BodyTube` | `print` | Standalone part; children are annotated relative to this tube | | `TrapezoidFinSet` / `EllipticalFinSet` / `FreeformFinSet` | `fuse` | Integrated into parent body tube; thickness bumped to minimum 12.7 mm; fillet clamped to `min(thickness * 0.25, 3.0 mm)` | | `InnerTube` (motor mount) | `fuse` | Local wall thickening in parent body tube; overridable to `separate` | | `CenteringRing` | `skip` | Absorbed when motor mount is fused; printed separately when motor mount is `separate` | | `TubeCoupler` | `fuse` | Integral aft shoulder on parent body tube; overridable to `separate` | | `Parachute`, `MassComponent`, `LaunchLug`, `RailButton` | `purchase` | Non-structural / purchased items | ### Fin thickness and fillet clamping - Minimum fin thickness: 12.7 mm (overridable via `fin_thickness_mm`). - Fillet radius: `min(thickness * 0.25, 3.0 mm)` by default. The fillet is further clamped to `thickness / 2` to avoid OCC kernel failures. This matches the known OCC limitation where root fillets above `~thickness/2 * 0.9` or `3 mm` fail. ## Annotation Flow ``` openrocket_component(action="read") | v gui/component_tree.json (unannotated) | v manufacturing_annotate_tree(fusion_overrides) | v annotate_dfam(tree, fusion_overrides) - walks stages -> components -> children - calls type-specific annotators (_annotate_nose_cone, _annotate_fin_set, etc.) - populates component.agent with AgentAnnotation | v gui/component_tree.json (annotated, written back in place) ``` ## Fusion Overrides The `fusion_overrides` parameter on `manufacturing_annotate_tree` accepts a dict with the following keys: | Key | Type | Default | Effect | |-----|------|---------|--------| | `motor_mount_fate` | `"fuse"` or `"separate"` | `"fuse"` | Whether motor mounts are fused as wall thickening or printed separately | | `coupler_fate` | `"fuse"` or `"separate"` | `"fuse"` | Whether couplers become integral shoulders or separate parts | | `nose_cone_hollow` | `bool` | `false` | Whether the nose cone is hollow (with wall thickness) | | `nose_cone_wall_mm` | `float` | `3.0` | Wall thickness when hollow | | `nose_cone_shoulder_length_mm` | `float` | `30.0` | Length of the integral nose cone shoulder | | `fin_thickness_mm` | `float` | `12.7` (minimum) | Override the minimum fin thickness | | `fin_fillet_mm` | `float` | computed | Override the fillet radius (still clamped to `thickness / 2`) | ## Comment Persistence Agent annotations are stored in OpenRocket component comment fields using the `== agents ==` delimiter format: ``` User's design notes here. == agents == fate: print fused_into: lower_body_tube reason: Fins always integrated into parent body tube for AM dfam_thickness_mm: 12.7 dfam_fillet_mm: 3.0 updated_by: manufacturing updated_at: 2026-04-10T12:00:00+00:00 ``` This format ensures annotations survive `.ork` regeneration: - `parse_comment()` splits the comment at the `== agents ==` delimiter, preserving human notes above and parsing key-value pairs below into an `AgentAnnotation`. - `serialize_comment()` rebuilds the full comment string from `(human_notes, AgentAnnotation)`. - On each `openrocket_component` (action="read") call, annotations are re-parsed from the `.ork` comments, so manual edits to the `.ork` file are respected.