AI-powered parametric furniture modeling for Fusion 360. Generates production-ready CAD models with real joinery from natural language, images, or reference...
---
name: woodworking
description: AI-powered parametric furniture modeling for Fusion 360. Generates production-ready CAD models with real joinery from natural language, images, or reference links.
version: 0.6.1
metadata:
openclaw:
requires:
bins: [git]
anyBins: []
env: []
primaryEnv: ""
emoji: "๐ชต"
homepage: https://github.com/ShopPrentice/shopprentice
os: ["macos", "linux", "windows"]
install:
- kind: brew
formula: git
bins: [git]
security:
networkAccess:
- description: "MCP JSON-RPC server on localhost:9100 for live Fusion 360 script execution"
host: "localhost"
port: 9100
direction: "local-only"
installMethod:
- description: "One-line installer clones the GitHub repo and symlinks the Fusion 360 add-in. Source is fully auditable at https://github.com/ShopPrentice/shopprentice/blob/main/install.sh"
command: "curl -sSL https://raw.githubusercontent.com/ShopPrentice/shopprentice/main/install.sh | bash"
codeExecution:
- description: "The skill generates Fusion 360 Python scripts and executes them via the MCP add-in. Scripts are saved locally and can be reviewed before execution. Without the add-in, the skill still generates correct scripts โ users can run them manually."
compatibility:
recommended: ["Claude Opus"]
tested: ["Claude Opus (claude-opus-4-6) via Claude Code"]
note: "This skill requires frontier-level LLMs with strong long-context reasoning and code generation. Developed and tested with Claude Opus. Other models are untested and may fail to follow the multi-step procedural instructions."
---
# Fusion 360 Parametric Furniture Modeling
You are generating a Fusion 360 Python script to build a parametric furniture model. Follow these rules strictly.
## Before You Start: Pick the Mode
Before writing any code, decide whether you are **building from scratch** or **adding to an existing model**. Call `capture_design` to see the current document state:
- **Empty document** โ ground-up build. Use `execute_script(clean=True)` each phase; Ctrl+Z reverts.
- **Existing model you built in this session (tracked)** โ iterate by editing the script and re-running `execute_script(clean=True)`.
- **Existing model the user built manually, or from a script you don't have** โ **additive mode**. Do NOT use `clean=True` โ it would wipe the user's work. Call `execute_script` WITHOUT `clean`, looking up bodies by name via `root.allOccurrences` and appending features to the timeline. Read `woodworking/mcp-advanced.md` (Approach 2) for the full pattern before writing code.
- **Tracked model with unsynced UI changes** โ call `sync_script` first, then decide rebuild vs additive.
The `execute_script` tool enforces this at the tool level: `clean=True` is **rejected** on untracked or unsynced documents with a structured error telling you which mode to use. Treat the rejection as a signal that you picked the wrong mode โ adjust, don't reach for `force_clean=True` unless you truly intend to wipe the document.
## Design Philosophy: Think Like a Furniture Maker at the Fusion 360 UI
Before writing any code, plan the modeling steps the way an experienced designer would approach the Fusion 360 UI โ component by component, feature by feature. You are not a software engineer writing a program. You are a craftsperson building a piece of furniture, and the API is just your hands on the mouse.
1. **Plan before building.** Before writing code, outline every modeling step in order: which component, which feature, which replication strategy. Think: "If I were clicking through the Fusion 360 UI, what would I do next?" Write the plan as a step list (see Design-First Planning below).
2. **Build one, replicate the rest.** Prefer building one template and using **Mirror** and **Rectangular Pattern** features for the rest. If you find yourself reaching for a Python `for` loop to create geometry, stop โ use a Fusion 360 pattern instead. **Exception:** Per-corner joinery (dovetails, box joints) where CUT/JOIN targets differ per corner requires independent construction at each corner โ mirrors of CUT/JOIN extrudes inherit the original `participantBodies` reference and fail.
3. **Everything parametric.** When the user changes any dimension in Modify > Change Parameters, the entire model must recompute automatically โ lengths, mirror positions, pattern counts, everything.
4. **Always organize with components.** Group related bodies into named components (e.g., Sides, Shelves, Top, Kick โ or Case, Bottom, Lid for boxes). Features live inside their respective components; cross-component operations (like CUT) live in root via assembly proxies. Even small boxes benefit from component structure โ clearer timeline, feature isolation, and reusable assembly patterns.
5. **Feature-based modeling only.** Every shape is: Sketch > Constrain dimensions parametrically > Extrude. This creates timeline features that recompute when parameters change.
6. **If it fits, it cuts.** When body A sits inside body B, use A as a CUT tool to create its void in B โ never draw the void as a separate sketch. The body IS the perfect-fit shape: one source of truth, zero redundant geometry. This applies to any mechanical mate, not just joinery:
- **Joinery:** tenon CUTs mortise, tail CUTs socket, tongue CUTs groove. Then JOIN the tenon/tail to its owning board.
- **Panels:** lid CUTs its slot in the front board, bottom panel CUTs its groove in each case board.
- **Openings:** door CUTs its frame opening, drawer front CUTs its cavity, sliding panel CUTs its track.
- **Hardware/inserts:** wedge CUTs its socket, hinge leaf CUTs its recess, inlay CUTs its pocket.
**Recognition rule:** if you're about to sketch a void that matches an existing body's shape, stop โ CUT the body instead (`keepTool=True`). If the fitting body also joins a parent, CUT first, then JOIN.
7. **No overlapping bodies.** Two physical bodies can never occupy the same space. When bodies share volume, one must CUT the other (rule 6). This must hold not just at script time but **across all valid parameter changes** โ if the user increases `lid_thick`, the lid must not collide with the case boards. Achieve this by defining body positions and sizes in terms of shared parameters so they stay in agreement:
- **Derive, don't hardcode boundaries.** A lid at Z = `open_height` with thickness `lid_thick` means `open_height` must equal `box_height - lid_thick`. If both are independent parameters, the user can set values that overlap.
- **Use CUT to enforce fit.** When body A fits inside body B, CUT A into B (rule 6). The void updates automatically when A's dimensions change โ no overlap possible.
- **Validate with `check_interference`** after every phase. Clean designs have zero interferences at any parameter value, not just the defaults.
8. **Build order matters.** Cut grooves and dados **before** joining corner joinery (dovetails, box joints). Side boards span only their initial footprint before tails are joined; groove tool bodies that extend beyond the board only CUT the material that exists at that moment. When tails are later joined, they attach ungrooved โ producing clean, stopped grooves at corners with zero extra geometry. This "implicit stopped groove" technique eliminates manual stop calculations.
9. **Think in grain direction and mechanical interlock.** Wood is a directional fiber material โ fibers (bonded by lignin) run parallel to the longest dimension of each part: leg fibers in Z, rail fibers in X or Y, stretcher fibers along their length. This has three consequences for every joint:
- **End grain glue is weak.** Where fiber ends meet a surface (end grain to side grain), glue alone provides almost no holding force.
- **Mechanical joints use fiber strength.** When a tenon sits inside a mortise, the wood fibers of both pieces resist pulling apart โ strong even without glue.
- **Side grain to side grain glue is strong.** A tenon inside a socket creates side-grain contact surfaces where glue forms a bond as strong as the wood itself.
**During planning, audit every connection:** wherever two parts meet, ask "if I built this in real wood, would gravity or use pull it apart?" If the answer is yes, there must be a physical joint โ M&T, domino, dovetail, etc. โ not just touching surfaces. The model must show the interlock: a tenon body occupying a mortise void, a tail body filling a socket. A CUT that creates a void is only half the joint โ the mating piece must physically fill it.
**Grain direction determines joint choice:**
- **Long grain to long grain** (parallel fibers meeting side-to-side) โ glue alone is sufficient (edge-joining boards for a panel).
- **End grain to side grain** (fiber ends meeting a surface) โ mechanical joint required (rail into leg = M&T, board corner = dovetail).
- **End grain to end grain** โ weakest possible bond. Always reinforce with a cross-grain element (spline, domino, biscuit).
**Wood movement determines attachment method:**
- Wood expands/contracts across the grain (perpendicular to fiber direction). Narrow parts (legs, rails) are negligible. Wide panels (desk tops, table tops, seats > ~6") move measurably with seasonal humidity changes.
- **Never rigidly attach a wide panel to a cross-grain apron.** Dominos, dowels, or screws through fixed holes lock the panel โ when it shrinks, the cross-grain apron holds it in tension, splitting it.
- **Use slotted fasteners** for cross-grain top-to-apron connections: `tabletop_bracket` (L-bracket with slotted screw holes), Z-clips, or figure-8 fasteners. The slot allows the panel to slide across the grain while staying flat.
- **Rigid attachment is OK** when the apron runs WITH the grain (both parts move together).
## Topic Reference
This skill is modular. The core (this file) covers fundamentals needed for every project. **Read topic files ONLY when you need them** โ do NOT pre-load all files at the start. Read the type + style file during planning. Read joinery files only when writing joinery code. Read other topics only when the specific situation arises.
### Topic Files
| Topic | When to Read | Status | File |
|-------|-------------|--------|------|
| **Angled Construction** | Splayed legs, stretchers/rails on splayed legs, through-tenons, compound angles, Sweep, Move, SplitBody | Tested (counter stool) | `woodworking/angled-construction.md` |
| **Details & Finishing** | Fillets, chamfers, edge treatments (Phase 3) | Planned โ inline quick reference below | `woodworking/details-and-finishing.md` |
| **MCP Advanced** | Modifying existing designs, fixing dimensions, adding features to built models, delete-and-rebuild timeline sections | Tested (bar side table) | `woodworking/mcp-advanced.md` |
| **Appearance** | Applying wood species, grain direction, multi-species designs โ read before calling `apply_appearance`. Includes the `# APPEARANCE SPEC` comment-block convention for persisting grain overrides / multi-pass finish across `execute_script(clean=True)` rebuilds | Tested (blanket box) | `woodworking/appearance.md` |
| **Hardware Installation** | Importing STEP hardware (bed rail fasteners, hinges), positioning, caching, direction detection, component organization | Tested (queen + twin beds) | `woodworking/hardware-installation.md` |
| **Joinery Rules** | Combine-based joinery, tooling bodies, edge rabbets, cross-component CUT patterns | Tested | `woodworking/joinery.md` |
| **Screenshots** | Camera positioning, standard shots, transparent views, detail framing | Tested | `woodworking/screenshots.md` |
| **Incremental Updates & Build Strategy** | Build order, component-by-component workflow, document management, script epilogue, interactive editing, rebuild-vs-patch | Tested | `woodworking/incremental-updates.md` |
| **Replication & Common Errors** | Mirror, Pattern, body pattern ghost bodies, mirror+pattern limitation, 24-row error table | Tested | `woodworking/fusion-api-rules.md` |
| **Helpers Reference** | `sp.*` function signatures, `sketch_rect_model`, `ev()`, feature builders | Tested | `woodworking/helpers-reference.md` |
| **Organic Shapes** | Self-contained designer + recipe doc for sculpted forms. Shape taxonomy (5 classes): (1) turned/spindled parts โ revolve, (2) flat-plan outlines โ closed spline + extrude, (3) 3-D organic solids (lens-profile seats, rounded finial tips) โ multi-section loft + tangent end conditions, (4) sculpted dish/saddle โ sphere CUT, (5) character surfaces โ Form T-splines (out-of-scope for scripting). Classes 1โ4 include inline API snippets; also covers the approximateโrefineโcapture iteration loop and through-tenon trimming on organic surfaces | Tested (Esherick stool) | `woodworking/organic-shapes.md` |
| **Loft** | Deep feature reference for advanced loft variants: closed-ring topology, rail/centerline guides, 1โNโ1 branching manifolds, surface-only and loft-as-cut variants, closed-spline cross-section generators (kidney/star/cardioid), all end-condition types. **Don't preload** for common organic shapes โ use the inline recipes in `organic-shapes.md` instead. Read this file only when a build actually needs one of these variants | Tested (18 fixtures) | `woodworking/loft.md` |
### Joinery Reference Files
Read the specific joint file **before writing joinery code**. Each file has parameters, geometry workflow, replication strategy, and pitfalls. Choose the joint type based on grain orientation at the interface (rule 9) โ end-grain-to-side-grain connections need mechanical interlock (M&T, dovetail, domino), while long-grain-to-long-grain can use glue alone.
**Status key:** "Tested" means the technique was built end-to-end in a real model, hitting and resolving actual API pitfalls. "Draft" means the file has plausible instructions but hasn't been validated through a real build โ expect missing pitfalls and possible wrong API sequences. When using a Draft file, validate each step with `capture_design` and be ready to debug.
| Joint | When to Read | Status | File |
|-------|-------------|--------|------|
| **Mortise & Tenon** | Leg-to-rail, stretcher-to-leg, frame-and-panel, table aprons, any rail-into-post connection | Tested (counter stool โ blind, through & angled variants) | Inline in skill + `mortise_tenon` template |
| **Drawbore M&T** | Stretcher-to-leg with offset pins for permanent tightness โ workbenches, trestle tables, timber frames | Tested (Roubo workbench โ through & blind variants) | `woodworking/joinery/drawbore.md` + `drawbore` template |
| **Domino** | Hidden structural joints, kick boards, shelf-to-back, panel alignment โ any time you need a loose tenon | Tested (counter stool, bookshelf) | `woodworking/joinery/domino-joint.md` |
| **Dovetail** | Drawer fronts, premium boxes, visible corner joints where mechanical strength matters | Tested (pencil box, wrap box) | `woodworking/joinery/dovetail.md` |
| **Box Joint** | Boxes, drawers, decorative interlocking corners โ simpler alternative to dovetails | Draft | `woodworking/joinery/box-joint.md` |
| **Dado & Rabbet** | Shelves into sides, case backs, drawer bottoms, any panel-into-groove connection | Tested (bookshelf, template fixtures โ through/stopped dado, rabbet, panel groove) | `woodworking/joinery/dado-rabbet.md` |
| **Bridle Joint** | Frame corners, T-connections, open mortise-and-tenon at end of a rail | Draft | `woodworking/joinery/bridle-joint.md` |
| **Lap Joint** | Flat frames, cross braces, grid assemblies, half-lap at crossings | Draft | `woodworking/joinery/lap-joint.md` |
| **Miter Joint** | Picture frames, trim, hidden end grain at corners | Draft | `woodworking/joinery/miter-joint.md` |
| **Spline Joint** | Reinforced miters, decorative accents across a joint line | Draft | `woodworking/joinery/spline-joint.md` |
| **Dowel Joint** | Edge joining, panel glue-ups, face frames, spindle-to-rail, round-peg alignment | Tested | `woodworking/joinery/dowel-joint.md` + `woodworking/templates/dowel.py` |
| **Pocket Hole** | Face frames, quick assemblies, tabletop attachment โ screw-based | Draft | `woodworking/joinery/pocket-hole.md` |
| **Bed Rail Fastener** | Bed rail to post โ detachable STEP hardware (mortise bedlock, hooks + slots) | Tested (queen + twin beds) | `woodworking/templates/bed_rail_fastener.py` + `woodworking/hardware-installation.md` |
| **Tenon Wedge** | Through tenon tightening, fox wedging (blind tenons), Windsor spindle/stretcher locking โ rect (2 wedges) or round (1 centred, trimmed to cylinder). Grain detected via principal axes of inertia; pass `grain_dir=` for ambiguous mortise pieces (seats, slabs) | Tested (Windsor chair โ splayed legs + angled stretchers) | `woodworking/joinery/tenon-wedge.md` + `tenon_wedge` template |
| **Bowtie / Butterfly Key** | Live edge slab crack stabilization, decorative inlay | Tested (twin bed) | `woodworking/templates/bowtie.py` |
**Read the topic/joinery file BEFORE writing code** that uses those techniques. The core skill provides the routing โ the reference files provide the implementation details. For Draft files, treat instructions as a starting point and validate aggressively.
### Style & Type Guides
Before planning, identify the **furniture type** and **design style** from the user's request. Load the matching files โ they provide component checklists, connection requirements, hardware needs, proportions, and detail patterns specific to that combination.
**Identify type** from what the user is building:
| Type | Keywords | File |
|------|----------|------|
| Chair | chair, dining chair, side chair | `woodworking/types/chair.md` |
| Stool | stool, counter stool, bar stool, step stool | `woodworking/types/stool.md` |
| Bench | bench, entryway bench, garden bench | `woodworking/types/bench.md` |
| Sofa | sofa, couch, settee, loveseat | `woodworking/types/sofa.md` |
| Dining table | dining table, farm table, harvest table | `woodworking/types/dining-table.md` |
| Coffee table | coffee table, cocktail table | `woodworking/types/coffee-table.md` |
| Side table | side table, end table, nightstand, accent table | `woodworking/types/side-table.md` |
| Desk | desk, writing desk, secretary | `woodworking/types/desk.md` |
| Console table | console, TV console, media console, credenza | `woodworking/types/console-table.md` |
| Chest | chest, trunk, blanket chest, toy box, hope chest | `woodworking/types/chest.md` |
| Box | box, pencil box, jewelry box, keepsake box | `woodworking/types/box.md` |
| Cabinet | cabinet, cupboard, pantry, hutch | `woodworking/types/cabinet.md` |
| Dresser | dresser, bureau, chest of drawers | `woodworking/types/dresser.md` |
| Bookshelf | bookshelf, bookcase, shelving unit | `woodworking/types/bookshelf.md` |
| Wardrobe | wardrobe, armoire, closet | `woodworking/types/wardrobe.md` |
| Sideboard | sideboard, buffet, server | `woodworking/types/sideboard.md` |
| Bed frame | bed, bed frame, platform bed, four-poster | `woodworking/types/bed-frame.md` |
| Crib | crib, baby crib, toddler bed | `woodworking/types/crib.md` |
| Planter | planter, window box, plant stand | `woodworking/types/planter.md` |
| Pergola | pergola, arbor, trellis, gazebo | `woodworking/types/pergola.md` |
| Mirror frame | mirror, mirror frame, looking glass | `woodworking/types/mirror-frame.md` |
| Shelf | shelf, floating shelf, wall shelf, ledge | `woodworking/types/shelf.md` |
**Identify style** from visual cues, user description, or reference photos:
| Style | Keywords / Visual Cues | File |
|-------|----------------------|------|
| Modern | clean lines, minimal, contemporary, square edges, hidden hardware | `woodworking/styles/modern.md` |
| Shaker | through dovetails, tapered details, applied base, brass hardware, simple lines | `woodworking/styles/shaker.md` |
| Craftsman | exposed tenons, corbels, quartersawn oak, thick stock, Arts & Crafts | `woodworking/styles/craftsman.md` |
| Mid-century | tapered legs, floating tops, thin profiles, hidden joinery, Danish, Scandinavian | `woodworking/styles/mid-century.md` |
| Rustic | thick boards, farmhouse, reclaimed, visible fasteners, breadboard ends | `woodworking/styles/rustic.md` |
| Nakashima | live edge, natural edge, slab, organic, bowties, butterfly keys, walnut slab, free-form | `woodworking/styles/nakashima.md` |
**If no style is specified or identifiable, default to Modern.**
**Read both files BEFORE the high-level plan.** The type file tells you what components and connections to plan. The style file tells you which joinery, edge treatments, and hardware to use. If a file doesn't exist yet, proceed with the core skill rules and note the gap.
## Parameter Planning
Choosing which values are user parameters vs. derived is critical. The goal: adjusting any single parameter always produces a clean, valid model โ no broken geometry, no asymmetric gaps, no overlapping bodies.
**Principle: parameterize the envelope and the parts; derive the fit.** Furniture dimensions form constraint chains โ for example, `table_h = leg_h + top_thick + gap`. When multiple dimensions are linked by a sum, make the physically meaningful ones user parameters and derive the leftover:
1. **Envelope dimensions** (overall height, width, depth) โ always user parameters. These are what the customer specifies or the maker measures in the room.
2. **Part dimensions** (leg height, rail width, stock thickness) โ user parameters when they represent a design choice the maker controls ("I want 26-inch legs", "I'm using 3/4-inch stock").
3. **Fit dimensions** (gaps, clearances, internal offsets) โ derived. These are whatever is left over after the envelope and parts are placed.
When a constraint chain has N terms, at most N-1 can be independent. Choose the least meaningful dimension to derive โ typically an internal gap or clearance that the maker doesn't independently decide.
**Example โ table height chain:**
- User params: `table_h` (overall height), `leg_h` (leg length), `top_thick` (stock choice)
- Derived: `top_gap = table_h - leg_h - top_thick` (clearance between leg top and tabletop underside)
- The maker decides the table height, leg length, and stock. The gap is a consequence โ not a design choice.
**Example โ box height chain:**
- User params: `box_height` (overall), `board_thick` (stock), `lid_thick` (stock), `bottom_thick` (stock)
- Derived: `open_height = box_height - board_thick - lid_thick - bottom_thick` (usable interior)
- Or alternatively: `open_height` is the user param and `box_height` is derived โ whichever the maker thinks in terms of.
**Principle: define count, derive spacing.** When elements repeat across a dimension (tails, slats, fingers), make the *count* a user parameter and derive the *spacing* from `board_dimension / count`. This guarantees elements always fill the space exactly. The alternative โ defining element width + gap width independently and using `floor()` to compute count โ leaves uneven remainders that break symmetry.
**Parametric positions (MANDATORY):** `ev()` is for approximate placement ONLY. Every `ev()` call that positions sketch geometry MUST be followed by `addDistanceDimension` with a parametric expression. Without this, geometry stays at stale positions when parameters change. This was the #1 source of broken models in testing โ dog holes, pins, and vise components all failed when parameters changed because they had `ev()` placement without parametric dimensions.
```python
# WRONG โ positions baked at script time, breaks on parameter change:
ctr = m2s(P.create(ev("mid_x"), ev("leg_d / 2"), ev("ls_z + ls_w")))
sk.sketchCurves.sketchCircles.addByCenterRadius(P.create(ctr.x, ctr.y, 0), r)
# only radial dimension โ center position is NOT parametric
# RIGHT โ ev() for placement, then parametric dimensions:
ctr = m2s(P.create(ev("mid_x"), ev("leg_d / 2"), ev("ls_z + ls_w")))
sk.sketchCurves.sketchCircles.addByCenterRadius(P.create(ctr.x, ctr.y, 0), r)
d.addRadialDimension(circle, ...).parameter.expression = "dog_dia / 2"
d.addDistanceDimension(origin, circle.centerSketchPoint, H, ...).parameter.expression = "mid_x"
d.addDistanceDimension(origin, circle.centerSketchPoint, V, ...).parameter.expression = "ls_z + ls_w"
```
**Face-relative sketching (MANDATORY):** Sketch positions must be relative to the features they interact with โ not absolute world coordinates. When a sketch CUTs or modifies a body, dimension from the body's face edges or a projected reference, not from the sketch origin with `leg_setback + ...`. For example, a tenon on a leg should reference the leg top face, not compute its position from `leg_setback`. When the leg moves, the tenon follows automatically through the face reference. Use `_face_fl_pt(sketch)` to get the face corner point for dimensioning, or project a construction plane from a face with `sp.off_plane(comp, face_proxy, "0 in", ...)` and dimension from the projected reference.
**How to decide:**
1. Ask: "If the user changes this value, does the model stay valid?" If increasing a width could overflow available space, that width should be derived from a count instead.
2. Ask: "Does changing this parameter require other values to adjust?" If yes, those other values must be derived expressions, not independent parameters.
3. Ask: "Is any geometry positioned using a value computed at script time?" If yes, add a sketch dimension with a parameter expression so it updates live.
4. Ask: "Would a maker write this dimension on a cut list or sketch?" If yes, it should be a user parameter. If it's just "whatever's left over" after other dimensions are placed, derive it.
**Example โ dovetails:** `dt_tail_w` (tail width) + `dt_tail_count` are user parameters. `dt_pin_w = board_h / dt_tail_count - dt_tail_w` is derived. Changing count or tail width always produces evenly-spaced tails with symmetric half-pins. If `dt_pin_w` were an independent parameter instead, the user could easily set values where tails don't fit the board.
## Design-First Planning
Before writing any code, output a **high-level plan** covering all components and their build order. This is a single text-only response โ no file writes, no code blocks longer than 5 lines.
Then, before each component's build cycle, output a **component plan** with the specific features for that component.
### High-Level Plan (one response, before any code)
```
Components: Sides, Shelves, Top, Kick
Build order: Sides โ Shelves โ Top โ Kick โ Cross-component CUTs โ Details
Parameters: board_thick, shelf_depth, shelf_count, total_height, ...
Midplanes: XMid (total_length/2), YMid (total_width/2)
Joinery: M&T shelves into sides, dado for kick
Grain & joints:
Sides: grain in Z (vertical) โ end grain meets shelf side grain โ M&T
Shelves: grain in X (horizontal) โ tenons into side mortises
Kick: grain in X โ dado into sides (cross-grain housing)
```
### Component Plan (one response per component, before its build cycle)
```
Shelves component (cycle 3):
- Construction planes: shelf offset
- Extrude ONE shelf body (NewBody)
- Extrude ONE tenon (NewBody)
- Mirror tenon across YMid โ back tenon
- Mirror [tenon + mirror] across XMid โ right side tenons
- JOIN all 4 tenons into shelf body
- Body pattern shelf along Z (count=n_shelves, spacing=shelf_spacing)
Expected: n_shelves bodies in Shelves component
```
### Cross-Component Plan (after all components built)
```
Cross-component CUTs (root):
- CUT left side with ALL shelf proxies (keepTool=True)
- CUT right side with ALL shelf proxies (keepTool=True)
- CUT sides with kick proxies
```
Each step maps to exactly one Fusion 360 feature. No Python loops, no batch logic โ just the sequence a designer would follow in the timeline.
## Fusion 360 API Rules
```python
design.designType = adsk.fusion.DesignTypes.ParametricDesignType
```
Set this BEFORE accessing `design.userParameters`. Without it: `RuntimeError: this is not a parametric design`.
### Do NOT Use
- `TemporaryBRepManager` โ creates static geometry inside `BaseFeature` blocks. Parameters exist in Change Parameters but changing them does NOT update geometry.
- `createByReal(value_in_cm)` for parameter creation โ shows confusing cm values in the UI.
- Python `int()` at script time for pattern counts โ use `floor()` in parameter expressions instead.
- **Python `for` loops for geometry replication** โ use Rectangular Pattern or Mirror features instead. A `for` loop creates N independent features that don't update when count changes. A pattern is one parametric feature that recomputes automatically. **Note:** Bodies with CUT/JOIN history create ghost bodies when patterned โ see Body Pattern Ghost Bodies under Replication Strategy for how to handle this.
### User Parameters
Create with `ValueInput.createByString("60 in")` so Change Parameters shows readable values:
```python
params.add("total_length", adsk.core.ValueInput.createByString("60 in"), "in", "Overall length")
```
### Derived Parameters
Use expression strings referencing other parameters. These auto-recompute:
```python
params.add("shoulder_length",
adsk.core.ValueInput.createByString("total_length - 2 * leg_size"),
"in", "Shoulder length between legs")
```
### Dimensionless Parameters (counts)
For counts derived from `floor()`, use empty string `""` as the unit:
```python
params.add("n_slats", adsk.core.ValueInput.createByString("floor(shoulder_length / slat_width)"), "", "Number of slats")
```
These update automatically when referenced dimensions change.
### Sketch Plane Selection
Two valid approaches, depending on the project:
**Approach A: Sketch on body faces.** When creating a feature that relates to an existing body (joints, pockets, decorative details), find the relevant face on that body and sketch directly on it. The sketch plane inherits the body's position โ no construction plane offset to keep in sync.
```python
def find_face(body, axis, direction):
"""Find outermost planar face along axis in direction (+1=max, -1=min).
Uses abs(normal) because face.geometry.normal doesn't always match
the outward normal โ it's the mathematical plane normal."""
best = None
best_val = -1e10 if direction > 0 else 1e10
for i in range(body.faces.count):
face = body.faces.item(i)
geom = face.geometry
if isinstance(geom, adsk.core.Plane):
if abs(getattr(geom.normal, axis)) > 0.9:
fv = getattr(face.pointOnFace, axis)
if (direction > 0 and fv > best_val) or (direction < 0 and fv < best_val):
best_val = fv
best = face
return best
# Example: sketch on the front face (min-Y) of a rail body
front_face = find_face(rail_body, "y", -1)
sk = comp.sketches.add(front_face)
```
Also available as `sp.find_face(body, axis, direction)`.
**Clean references before profile selection (MANDATORY):** Any sketch on a face or with `sketch.project()` calls has reference lines that split profiles into fragments. **Always call `sp.refs_to_construction(sk)` after dimensioning but before selecting a profile.** This converts reference/projected lines to construction geometry โ they keep their sketch points (valid for dimensions) but stop forming profile boundaries. Then `sp.smallest_profile(sk)` returns the correct drawn profile. Omitting this step is the #1 cause of wrong-profile extrusions.
```python
# After all sketch geometry and dimensions are complete:
sp.refs_to_construction(sk)
prof = sp.smallest_profile(sk)
ext = sp.ext_new(comp, prof, "depth", "MyFeature")
```
**Extrude direction on body faces:** The default (positive) extrude direction on a face sketch follows `face.evaluator.getNormalAtPoint()` โ the true outward normal, pointing AWAY from the body. Use `flip=True` (NegativeExtentDirection) for CUT extrudes on body faces so the cut goes INTO the body.
**Coincident geometry on body-face sketches:** When sketch lines fully coincide with face boundary edges (e.g., an arch baseline at the face corner), Fusion merges them and fails to create separate profiles. Fix: project the face edge via `sk.project(edge)`, then draw the arc from the projected line's sketch points. The projected edge + arc properly split the face. Position dimensions become unnecessary since the projection is already parametric.
**Axis mapping on non-XY planes (MANDATORY):** On construction planes and body faces, sketch H and V map to different model axes than expected. **Always use `sp.probe_orientations()` to get the correct `DimensionOrientation` for each model axis.** Never hardcode H/V assumptions.
```python
# One-liner: returns {'x': H_or_V, 'y': H_or_V, 'z': H_or_V}
orient = sp.probe_orientations(sk, ev("cx"), ev("cy"), ev("cz"))
# Use the dict to assign the correct orientation per model axis:
d.addDistanceDimension(origin, pt, orient['z'], placement
).parameter.expression = "ls_z + ls_w / 2"
d.addDistanceDimension(origin, pt, orient['y'], placement
).parameter.expression = "leg_d / 2"
```
This replaces `probe_sketch_axes` and `probe_sketch_signs` โ it returns the orientation enum directly, which is what `addDistanceDimension` needs. No manual axis detection code required.
`sketch_rect_model` and `sketch_slot_model` handle axis mapping internally. Use `probe_orientations` only for custom sketch geometry (circles, manual rectangles) where you add dimensions yourself.
**Sketch plane preference (follow this order):**
1. **Existing body face (preferred).** If a planar face already exists at the needed location, sketch on it. This is how a designer works in the UI โ click the face, start sketching. No construction plane needed. Use `sketch_rect_model` with the face as the plane argument; it works on BRepFaces the same as on construction planes.
2. **Construction plane (only when required).** Use only when one of these applies:
- **No body exists yet** โ first body in a component has no face to sketch on.
- **Midplane for Mirror or Pattern** โ no face exists at the midpoint.
- **Sketch will be mirrored** โ face-based sketches CANNOT be mirrored. MirrorFeature fails with NO_TARGET_BODY because the mirror can't find an equivalent face on the mirrored side.
- **Root-level sketch on a component body** โ assembly proxy faces CANNOT host sketches. `comp.sketches.add(proxy_face)` throws `RuntimeError: invalid argument planarEntity`. Root-level cross-component operations must use construction planes.
**During design-first planning, audit every sketch plane:** for each sketch in the plan, ask "does a body face already exist here?" If yes, use it. Only reach for a construction plane if one of the four exceptions above applies. Fewer construction planes = cleaner timeline, faster recompute, and geometry that moves parametrically with the body it belongs to.
### Sketch + Extrude Workflow
```python
# 1. Sketch with approximate geometry
sk = comp.sketches.add(plane)
rect = sk.sketchCurves.sketchLines.addTwoPointRectangle(p1, p2)
# 2. Add geometric constraints FIRST โ H/V constraints lock line orientation
gc = sk.geometricConstraints
gc.addHorizontal(rect[0])
gc.addHorizontal(rect[2])
gc.addVertical(rect[1])
gc.addVertical(rect[3])
# 3. Constrain dimensions parametrically
d_w = sk.sketchDimensions.addDistanceDimension(...)
d_w.parameter.expression = "slat_width" # linked to user parameter
# 4. Extrude with parametric distance
ext_input = comp.features.extrudeFeatures.createInput(profile, operation)
ext_input.setDistanceExtent(False, adsk.core.ValueInput.createByString("body_height"))
```
### Geometric Constraints on Sketch Lines (CRITICAL)
**Every sketch line that should be horizontal or vertical MUST have an explicit geometric constraint.** `addTwoPointRectangle` and `addByTwoPoints` create lines at the correct positions initially, but without explicit `addHorizontal`/`addVertical` constraints, lines can skew when parameters change โ rectangles become parallelograms, horizontal edges tilt.
**Rule:** After creating any sketch line, ask: "Should this line stay horizontal or vertical when parameters change?" If yes, add the constraint. Omit H/V constraints on:
- Intentionally angled lines (tapers, chamfer profiles, etc.)
- Arch baselines where both endpoints share the same model Z (already horizontal by construction). On offset planes, `addHorizontal` can perturb arc geometry enough to split thin bodies via CUT.
```python
# Rectangle โ constrain all 4 sides
rect = sk.sketchCurves.sketchLines.addTwoPointRectangle(p1, p2)
gc = sk.geometricConstraints
gc.addHorizontal(rect[0]) # bottom
gc.addHorizontal(rect[2]) # top
gc.addVertical(rect[1]) # right
gc.addVertical(rect[3]) # left
# Arch baseline โ DO NOT constrain. Both endpoints share the same Z
# (model coordinate), so the line is already horizontal. Adding addHorizontal
# on offset planes can perturb the arc geometry, causing the CUT to split
# thin bodies. The arc's shared sketch points (endSketchPoint/startSketchPoint)
# keep the profile closed without constraints.
arch_line = sk.sketchCurves.sketchLines.addByTwoPoints(p1, p2)
sk.sketchCurves.sketchArcs.addByThreePoints(
arch_line.endSketchPoint, mid_pt, arch_line.startSketchPoint)
# Taper triangle โ constrain the H and V edges, leave the angled line free
# IMPORTANT: H/V constraints are in SKETCH space, not model space.
# On XZ planes: model-X โ sketch-H, model-Z โ sketch-V (inverted)
# On YZ planes: model-Z โ sketch-H (inverted), model-Y โ sketch-V
# A line that is "horizontal in model" (same Z, varying X or Y) may be
# VERTICAL in sketch space on YZ planes. Always check probe_sketch_axes
# or modelToSketchSpace to determine the correct constraint direction.
bot = lines.addByTwoPoints(sa, sb) # same Z, varies in X or Y
lines.addByTwoPoints(sb, sc) # angled taper โ NO constraint
vert = lines.addByTwoPoints(sc, sa) # same X or Y, varies in Z
# XZ plane example (model-X โ sketch-H, model-Z โ sketch-V):
sk.geometricConstraints.addHorizontal(bot) # bot varies in model-X โ sketch-H
sk.geometricConstraints.addVertical(vert) # vert varies in model-Z โ sketch-V
# YZ plane example (model-Y โ sketch-V, model-Z โ sketch-H):
sk.geometricConstraints.addVertical(bot) # bot varies in model-Y โ sketch-V
sk.geometricConstraints.addHorizontal(vert) # vert varies in model-Z โ sketch-H
```
### Extrude Operations
| Operation | Use For |
|-----------|---------|
| `NewBodyFeatureOperation` | New bodies (legs, rails, slat bodies) |
| `CutFeatureOperation` | Mortises, grooves (removing material) |
| `JoinFeatureOperation` | Tenons, tongues (adding material to existing body) |
### participantBodies (CRITICAL)
When doing Cut or Join near other bodies, you MUST specify which body to target:
```python
ext_input.participantBodies = [target_body] # Python list, NOT ObjectCollection!
```
Using `ObjectCollection` causes `TypeError`. Using no participant bodies causes accidental merging or cutting of adjacent bodies.
### Fillet and Chamfer Features
> **Full reference:** `woodworking/details-and-finishing.md` โ edge selection strategies, chamfer types, code patterns, sizing constraints.
Quick reference:
- **Fillet:** `filletFeatures.createInput()` -> `inp.addConstantRadiusEdgeSet(edges, radius, propagate)`
- **Chamfer:** `chamferFeatures.createInput2()` -> `inp.chamferEdgeSets.addEqualDistanceChamferEdgeSet(edges, distance, propagate)`
- Note: chamfer uses `createInput2()` (not `createInput()`) and has a nested `.chamferEdgeSets` collection.
- The API requires `BRepEdge` objects, never `BRepFace`. Iterate face edges and deduplicate via `tempId`.
> **Replication Strategy & Common Errors:** `woodworking/fusion-api-rules.md` โ Mirror, Pattern, body pattern ghost bodies, mirror+pattern limitation, typical replication sequence, 24-row error table.
## Standard Helpers
> **Full reference:** `woodworking/helpers-reference.md` โ all `sp.*` function signatures, `sketch_rect_model` usage and limitations, `ev()` semantics, feature builder table.
Scripts use `from helpers import sp` and `ctx = sp.DesignContext()`. Key functions: `sketch_rect_model`, `ext_new`, `ext_op`, `combine`, `mirror_body`, `mirror_feats`, `body_pattern`, `off_plane`, `make_comp`, `find_face`, `probe_orientations`.
## Joinery Rules
> **Full reference:** `woodworking/joinery.md` โ combine-based workflow, tooling bodies, edge rabbets, cross-component CUT, bulk CUT, timeline ordering. Joint-specific files: see Joinery Reference Files table above.
**Core principle:** Build the tenon/tail as a body, CUT the receiving board (`keepTool=True`), JOIN to the owner. Timeline order: CUT first (root, assembly proxies), JOIN second (owning component). Cross-component: use `body.createForAssemblyContext(occ)` for CUT in root.
**Templates:** `mortise_tenon`, `domino`, `dovetail`, `finger_joint`, `half_blind_dovetail`, `splayed_legs`, `dowel`, `drawbore`, `tenon_wedge`, `dovetailed_drawer`. Use for joints with 4+ features; write inline for dado/rabbet/T&G. See `woodworking/joinery/README.md`.
**Hardware:** use `hardware.recommend_hinge()` + `hardware.install_butt_hinge()` for hinges, plus the `pull` and `chest_lock` templates for non-hinge hardware. See `woodworking/hardware-installation.md`.
## Incremental Build Strategy
> **Full reference:** `woodworking/incremental-updates.md` โ component-by-component build order, what-goes-where, document management, script epilogue, interactive editing, rebuild-vs-patch.
**Script location:**
- **ALWAYS create scripts in `~/shopprentice-projects/`.** Create the directory if it doesn't exist. Each project gets a subfolder named after the piece (e.g., `~/shopprentice-projects/dovetailed-box/`).
- **NEVER modify files in `~/.shopprentice/repo/`** โ that is the installed skill/add-in, not a project directory. The `examples/` folder there is read-only reference material.
- If an example script is relevant, READ it for reference but write the new script to the project folder.
**Project structure:** Each project folder contains:
```
~/shopprentice-projects/dovetailed-box/
dovetailed_box.py # Fusion 360 parametric script
README.md # Auto-generated project doc (see below)
```
**README.md (MANDATORY):** After completing a build (or at the end of each session), write/update a `README.md` in the project folder with:
- **Description** โ what was built, key design decisions
- **Status** โ Complete / In Progress (what's done, what's remaining)
- **Parameters** โ key user parameters and their current values
- **Build notes** โ any issues encountered and how they were resolved
- **Screenshots** โ paths to product shots if taken
This allows the user (or a new agent session) to resume work by reading the README to understand the project state. When resuming, ALWAYS read the project README first before making changes.
**Key rules:**
- **NEVER write more than one component's code per response.** Write Case โ execute โ validate โ THEN write Bottom โ execute โ validate. Do NOT bundle multiple components in one code generation. Small pieces (< 8 bodies) may combine structure + joinery but still validate between components.
- Auto-proceed on success โ do not wait for user approval between components.
- Same `.py` file, growing content. Cross-component CUTs are a separate cycle. Details last.
- Always end with `sp.apply_appearance()` + `get_product_shots`.
- Replace, don't patch โ when rewriting code, remove the entire old block.
## MCP Live Execution
> **Full reference:** `woodworking/mcp-advanced.md` โ MCP tool table, execution + validation loop, error retry rules, sandbox mode, timeline rollback diagnosis, modifying existing designs.
**Default behavior:** When MCP is available, ALWAYS execute automatically after generating code. Do not wait for user to ask.
**Loop:** execute_script โ on error: fix + retry (max 3 per error) โ on success: capture_design + validate_design (MANDATORY) โ auto-proceed.
**Final step:** apply_appearance โ get_product_shots โ present to user.
**Token efficiency:**
- `capture_design` returns a compact summary (body names + bounding boxes + params). Full capture saved to temp file (path in response) โ Read it only when deep inspection is needed.
- `get_product_shots` and `get_screenshot` save images to files and return file paths. **Do NOT Read the image files** โ just report the paths to the user. The user can open them directly.
- Prefer `validate_design` (text-only, ~100 tokens) over screenshots for intermediate validation.
<!-- SHOPPRENTICE_SCREENSHOT_MODE: none -->
**Screenshot mode: none** โ do NOT call `get_product_shots` or `get_screenshot` at any point. Use `validate_design` for all checks. Report validation results as text only. This setting overrides any screenshot instructions in topic files.
<!-- END_SCREENSHOT_MODE -->
## Component Structure Template
Table / Bookshelf:
```
Root
+-- Posts/Legs (build 1, mirror to all corners)
+-- LongRails (build front pair, mirror to back)
+-- ShortRails (build side pair, mirror to opposite)
+-- Panels/Slats (template per orientation, mirror + independent patterns)
+-- Top/Bottom (single panel)
(root timeline) bulk CUT features via assembly proxies
```
Box / Case:
```
Root
+-- Case (Front, Back, End_Left, End_Right)
+-- Bottom (bottom panel with edge rabbets)
+-- Lid (lid panel with edge rabbets)
(root timeline) panel-body groove CUTs, dovetails, dispensing slot
```
### Feature Ownership
| Where | What |
|-------|------|
| **Component** | Extrudes, mirrors, patterns, JOINs โ features that build the part |
| **Root** | Cross-component CUT features via assembly proxies |
## Construction Planes
All positioned with parametric offset expressions. Common planes:
- Body Z (visible area bottom)
- Upper/Lower rail planes
- Tongue planes (rail height minus groove depth)
- Midplanes for X and Y mirror operations
## Naming Convention
Name every feature and body for a readable timeline and easy debugging:
| Element | Pattern | Example |
|---------|---------|---------|
| Bodies | `Part` | `Front`, `Side_Left`, `Bottom`, `Lid` |
| Sketches | `Part_Sk` or `Feature_Sk` | `Front_Sk`, `BGL_Sk`, `DT_FL_Sk` |
| Extrudes | `PartBoard` or `Feature` | `FrontBoard`, `BGL`, `BottomLip` |
| Patterns | `Feature_Pat` | `DT_FL_PatCut`, `DT_FL_PatJoin` |
| Planes | `Part_Pl` or `Feature_Pl` | `Back_Pl`, `BG_Pl`, `LidLip_Pl` |
| Combines | `Feature_Cut` | `BGL_Cut`, `BGF_Cut` |
| Joinery | `JointType_Corner_Op` | `DT_FL_Cut`, `DT_BR_Join` |
| Fillets | `Part_Fil` | `Seat_Fil`, `LidEdge_Fil` |
| Chamfers | `Part_Ch` | `Lid_Ch`, `LegBottom_Ch` |
## Verification Checklist
1. Component tree shows logical grouping (or root-only for small pieces)
2. Timeline shows: build features > mirror, template > mirror > pattern
3. Change a major dimension > verify ALL sides update correctly
4. Change element width > verify counts increase/decrease on all sides
5. Section Analysis > verify joinery alignment
6. Verify no overlapping joints at corners
7. Body count matches expected (diagnostic print confirms no accidental merges or orphans)
8. **`validate_design` โ passed.** Single call checks connectivity (1 cluster) + interference (0 real overlaps). Fix disconnected clusters by adding mechanical joinery; fix interferences by checking CUT operations.
don't have the plugin yet? install it then click "run inline in claude" again.