Use when translating an OpenRocket logical design into a physical component tree for FDM/SLA 3D printing. Decides per-component fate (print, fuse, purchase, skip), applies AM-specific geometry patterns, and emits component_tree.json.
OpenRocket describes a rocket as a logical design — a tree of abstract components (nose cone, body tubes, inner tubes, couplers, fin sets, centering rings) that captures aerodynamic and mass properties. That description is manufacturing-agnostic.
The physical parts list — what actually gets produced and assembled — is a function of the chosen manufacturing method applied to that logical design. This skill handles the additive manufacturing case: converting OpenRocket components into a printable component tree that the generate-structures skill and cadsmith subagent use as their authoritative source.
Core principle: OpenRocket components don't map 1:1 to printed parts. A centering ring in OR becomes "localised wall thickening" in AM. A tube coupler in OR becomes "integral shoulder on the forward section" in AM. A fin set in OR becomes "fused geometry on the lower airframe" in AM. The translation is the whole point of this skill.
component_tree.json is missing or stale for the current .ork fileopenrocket_component(action="read", rocket_file_path=<path>, project_dir=<project_dir>) — authoritative mm-scaled geometry and the derived motor mount / body tube ID"additive" means print everything that can be printed and fuse aggressively; "hybrid" means print the nose cone and fin can but purchase body tubes and motor mount; "traditional" falls back to a separate skill not yet implementedA single JSON file written to <project_dir>/gui/component_tree.json by the manufacturing_annotate_tree MCP tool. The schema is enforced by Pydantic models at tool-call time, so malformed manifests are rejected before they reach disk. This file is the authoritative handoff to generate-structures and cadsmith — they consume it and produce STEP files from it. The mass-calibration skill also reads it to map filament_used_g back to OR components via component_to_part_map.
For most designs, the tool's defaults are correct and you can call it with no overrides. Review the design to identify whether any of the following ask-user conditions apply:
Motor mount fate — default is fuse (as local wall thickening).
separate if: motor total impulse ≥ 320 Ns (H-class+) AND the design has a payload bay or dual-deploy configuration suggesting the user might want motor swap-outs between flights.Coupler fate — default is fuse (as integral aft shoulder).
separate if: the design has a dedicated payload bay, dual-deploy, or any suggestion that the user plans to disassemble between flights.Retention mechanism — default is m4_heat_set for body diameters ≥ 38 mm, friction_fit below.
If the user has answered any of these, collect the answers into a fusion_overrides dict. Otherwise skip to step 2 and pass nothing.
For non-standard designs (transitions, boattails, multi-stage, cluster mounts), query rag_reference(action="search", collection="cad_examples", query=f"DFAM {distinctive_feature}", n_results=3). Fall through to defaults if no results; proceed silently on errors.
Call the manufacturing_annotate_tree MCP tool with action="generate":
manufacturing_annotate_tree(
action="generate",
project_root="<project_dir>",
rocket_file_path="<project_dir>/<rocket_name>.ork",
method="additive",
fusion_overrides=<dict if any, else omit>,
)
The tool:
openrocket_component (action="read") internally to get the mm-scaled component treeComponentTree Pydantic schema<project_root>/gui/component_tree.jsonDo not use the Write tool to hand-craft or edit the manifest. The Python implementation is the source of truth for the fusion rules; hand-writing is both slower (more context tokens) and error-prone (schema drift, typos, missing fields). The manufacturing_annotate_tree tool is the only way to produce a manifest. This includes detail modifications (holes, vents, rail button mounts, retention hardware) — these must be specified through tool parameters, not by directly editing component_tree.json.
These are the rules the tool applies. Documented here for understanding — do not re-implement them in the agent's reasoning. If you think the tool has applied a rule incorrectly, regenerate with a corrected fusion_overrides rather than post-editing the JSON.
| OR component type | Default fate (additive policy) | Notes |
|---|---|---|
NoseCone | print | One standalone part. Shoulder is always integral to the nose cone itself; retention mechanism is a separate decision. |
BodyTube | print | One part per section. Integrated features come from children (see below). |
TubeCoupler | fuse into parent section | Default fuse; overridable via coupler_fate: "separate". |
InnerTube (motor mount) | fuse into parent body tube | Default fuse as local wall thickening; overridable via motor_mount_fate: "separate". |
CenteringRing | absorb into wall thickening | When motor mount is fused, centering rings vanish entirely. When motor mount is separate, they become standalone parts. |
TrapezoidFinSet | fuse into parent body tube | Always fused for AM. Non-overridable; separate fin parts have poor layer adhesion. |
Parachute | skip | Non-structural assembly item. |
MassComponent | skip | Ballast, added at assembly time. |
LaunchLug / RailButton | skip | Adhesive-mounted at assembly time. |
Report the manifest at a glance before handing off to generate-structures:
Component tree for <rocket_name>:
Printed parts:
- nose_cone (from NoseCone)
- upper_airframe (from BodyTube:Upper, fused TubeCoupler:UpperAft)
- lower_airframe (from BodyTube:Lower, fused TrapezoidFinSet, fused InnerTube:MotorMount,
absorbed CenteringRing:Fore, CenteringRing:Aft)
Purchased items: (none)
Skipped: Parachute:Main, MassComponent:Nose Weight
Decisions:
- Motor mount: fused (LPR, no swap-out needed)
- Retention: M4 heat-set (64 mm diameter)
Total printed parts: 3
Stop and ask the user to confirm before generating CAD if any fusion decision was non-default or if the reference collection suggested an alternative.
These are the building blocks the generate-structures skill will use when writing each part's script. The DFAM skill's job is to specify them in the features block of each part; the CAD skill's job is to implement them in build123d.
The body tube has a base wall thickness; in the motor mount region, the inner wall steps inward to form the motor bore directly. Outer wall unchanged, inner wall variable along Z. One solid body, no glue joints.
Feature block:
{
"from": "InnerTube:MotorMount",
"as": "local_wall_thickening",
"bore_mm": 29.5,
"region_start_mm": 280,
"region_end_mm": 380
}
Fins extend radially from the outer wall of the body tube, filleted at the root for stress concentration reduction, as part of a single solid body. The number of fins and their angular spacing come from TrapezoidFinSet.
fillet_mm must be geometrically realisable: default thickness_mm / 4, hard-capped at min(thickness_mm / 2 * 0.9, 3 mm). A fillet approaching the fin half-thickness causes OCC to fail ("BRep_API: command not done") because the fillets on opposite broad faces collide. Do NOT copy thickness_mm into fillet_mm.
Feature block:
{
"from": "TrapezoidFinSet",
"as": "integrated_fins",
"count": 3,
"root_chord_mm": 80,
"tip_chord_mm": 40,
"span_mm": 60,
"sweep_mm": 25,
"thickness_mm": 3,
"fillet_mm": 1.5
}
The aft end of the forward section thickens outward into a tapered shoulder that mates with the next section's inner diameter. The shoulder's OD is the next section's ID minus clearance (typically 0.2 mm).
Feature block:
{
"from": "TubeCoupler:UpperAft",
"as": "integral_aft_shoulder",
"od_mm": 59.6,
"length_mm": 30,
"retention": {"type": "m4_heat_set", "count": 4}
}
Small internal ring at the forward end of the motor bore that prevents motor creep during flight. The motor case rests against this lip; the nozzle passes through.
Feature block:
{
"from": "InnerTube:MotorMount",
"as": "forward_stop_lip",
"bore_mm": 29.5,
"stop_id_mm": 26.0,
"thickness_mm": 3.0
}
A slightly raised flat region around each heat-set hole, for better thread engagement and cleaner drilling with the soldering iron.
Feature block:
{
"as": "heat_set_bosses",
"count": 4,
"angular_positions_deg": [45, 135, 225, 315],
"z_mm": 395,
"hole_diameter_mm": 5.7,
"hole_depth_mm": 7.0,
"boss_diameter_mm": 9.0,
"boss_height_mm": 1.5
}
parts list while the motor mount has fate "fuse" → contradiction, the rings should be in skipped_components with reason "absorbed into wall thickening"motor_mount part is generated when the parent body tube has fate "fuse" → contradictionfin_set.step file path appears anywhere → always wrong, fins must be integratedcomponent_to_part_map maps an OR component to no entry → the mass-calibration skill will fail to attribute its weight laterderived_from list doesn't reflect that → auditability brokenfillet_mm >= thickness_mm / 2 on an integrated_fins block → geometrically infeasible (OCC fillet self-intersects). The _fin_feature_block helper clamps to min(thickness_mm/2 * 0.9, _DFAM_MAX_FIN_FILLET_MM); apply the same clamp if authoring by hand. A fillet of 0 is also a red flag — the intent is "small but present" (thickness/4 capped at 3 mm).TubeCoupler between an adjacent pair → flat-ended tubes that can't interlock. The coupler is what DFAM fuses into an integral_aft_shoulder. Stop and add the coupler in the .ork before proceeding.TubeCoupler is marked fuse but the design is dual-deploy → may be wrong, ask the userretention="m4_heat_set" but no radial_holes modifications appear in the manifest → the design probably has no TubeCoupler to fuse into a shoulder. Retention modifications are tied to integral shoulders; without a shoulder there's nowhere to put the holes. Either add a coupler to the OR design or use a different mating strategy (friction fit on the nose cone shoulder, etc.){
"schema_version": 1,
"source_ork": "<absolute path to .ork file>",
"project_root": "<absolute path to project directory>",
"default_policy": "additive",
"generated_at": "<ISO 8601 timestamp>",
"directories": {
"scripts": "cadsmith/source",
"step": "cadsmith/step",
"gcode": "prusaslicer/gcode"
},
"parts": [
{
"name": "<snake_case part name>",
"script_path": "cadsmith/source/<name>.py",
"step_path": "cadsmith/step/<name>.step",
"gcode_path": "prusaslicer/gcode/<name>.gcode",
"derived_from": ["<OR component identifier>", "..."],
"fate": "print",
"features": {
"<feature_key>": "<value or nested object>"
}
}
],
"purchased_items": [
{
"derived_from": "<OR component identifier>",
"description": "<human-readable description>",
"suggested_source": "<vendor and part number if known>"
}
],
"skipped_components": [
{
"name": "<OR component identifier>",
"reason": "<short explanation>"
}
],
"assemblies": [
{
"name": "<assembly name>",
"step_path": "cadsmith/step/<assembly name>.step",
"parts_fore_to_aft": ["<part name>", "..."]
}
],
"decisions": [
{
"decision": "<decision key, e.g. motor_mount_fate>",
"policy_default": "<what the default would have been>",
"chosen": "<what was actually chosen>",
"reason": "<short explanation including any user input>"
}
],
"component_to_part_map": {
"<OR component identifier>": "<part name or 'skipped' or 'purchased'>"
}
}
derived_from entries use the OR component's type and name joined by a colon, e.g. "BodyTube:Upper Airframe", "TrapezoidFinSet:Trapezoidal Fin Set". When there's only one component of a type in the rocket, the bare type suffices.fate is one of "print", "purchase", or "skip". Fused components don't appear as their own parts — they're represented in skipped_components with a reason pointing at the part they were fused into, and the target part's derived_from includes them.directories is fixed per the project layout convention. Don't vary it per project.component_to_part_map is the authoritative lookup. Every OR component in the .ork file must appear as a key, mapped to either the name of the printed part it contributes to, the string "skipped", or the string "purchased". The mass-calibration skill uses this to attribute filament_used_g readings back to OR component mass overrides.assemblies is optional for now. Leave it as an empty list if not generating assembly STEPs.# the translation
openrocket_component(action="read") → {components, derived, handoff_notes}
↓
this skill applies policy + fusion rules
↓
gui/component_tree.json
↓
generate-structures skill → cadsmith scripts → STEP files
↓
prusaslicer → gcode → filament_used_g
↓
mass-calibration reads component_to_part_map and applies
override_mass_kg via openrocket_component
# the hard rules (reprise)
1. Fins are ALWAYS fused into the parent body tube
2. Centering rings vanish when the motor mount is fused
3. Every mated section needs a retention mechanism
4. Wall thickness ≥ 1.5 mm (FDM) or explicit user confirmation (SLA)