CadQuery assembly — two-phase orchestrator
Purpose
Produce a mechanically meaningful, manufacturable, visually verified CadQuery assembly. This skill is the orchestrator: it never writes part geometry directly. It owns decomposition, the shared dimension contract, workflow gates, and final export — and delegates every geometry pass to the sub-skills below.
Poor results usually come from skipping gates (no render, no user sign-off on Phase A), building all geometry in one file, duplicating features across passes, or placing parts with ad-hoc offsets instead of a shared common.py contract. This skill exists to prevent those failures.
Authoritative system prompt
All CadQuery code MUST follow every rule in:
/Users/dimiasael/leo/leo-monolith/resources/functions/system-messages/cad-generation.txt
Read it once at the start of the session. It defines imports, the result contract, Critical Pitfalls, boolean rules, the multi-file layout (common.py, build_<part>(), sub-assembly files, assembly.py), assembly mates, and mechanical interfaces.
Reference material
- Examples:
/Users/dimiasael/articraft/sdk/_examples/cadquery/*.md— read 2–3 closest matches (assemblies:raspberry_pi_3_model_b_assembly.md,a_parametric_enclosure.md,remote_enclosure.md). - API docs:
/Users/dimiasael/articraft/sdk/_docs/cadquery/*.md - Tessellate + render:
/Users/dimiasael/leo/sandboxed-python-runner/src/tessellate_snippet.py,render_snippet.py - Good in-repo reference layout:
/Users/dimiasael/leo/coffee_maker/(common.py,parts/,*_assembly.py,assembly.py)
Project layout (mandatory for Phase B onward)
Match cad-generation.txt multi-file structure — not a single monolithic script:
<project>/
common.py # ALL shared dimensions + coordinate convention + mate constants
high_level_assembly.py # Phase-A envelope baseline (kept for reference)
parts/
<function>.py # build_<function>() -> cq.Workplane; result = build_<function>()
<group>_assembly.py # build_<group>_assembly() -> cq.Assembly
assembly.py # entrypoint; result = root cq.Assembly
tools/
render_preview.py # optional; see cadquery-render
interference_audit.py # optional; see cadquery-interference-check
Rules:
- Every dimension that two parts share (hole PCD, bore Ø, stack height, snap ledge width) lives in
common.py— never duplicated in part files. - Part files import from
common.pyonly for shared values; local-only dims stay in the part file. - Sub-assemblies import part builders;
assembly.pyimports sub-assemblies only (no grandchild part imports). - Do not paste part geometry into
assembly.pyor sub-assembly files.
Sub-skill boundaries (do not blur these)
| Pass | Skill | Adds | Must NOT add |
|---|---|---|---|
| Envelope layout | cadquery-high-level-design |
Primitives, placement, decomposition | Holes, fillets, ribs, shells, fasteners |
| Functional | cadquery-part |
Primary solid, pockets, mating interfaces (bores, spigots, slots), hollow walls | Fillets, counterbores, engraving, procurement comments |
| Finish | cadquery-part-detailing |
Ribs, gussets, lead-in chamfers, cosmetic fillets | New holes, new mates, fasteners, vendor BOM text |
| DFM | cadquery-manufacturability |
Tap/clearance/counterbore, draft, process checks, vendor BOM, P/N engraving | Structural ribs (already in detailing) |
If a feature appears in the wrong pass, delete it from that file and re-run the correct pass — do not stack duplicates.
The two-phase flow
┌────────────────────────────────────────────────────────────────────────┐
│ PHASE A — TOP-DOWN (user-verified architecture) │
│ 1. decompose → part list + mates + process guesses │
│ 2. common.py (envelopes + coords + MATE_* constants) │
│ 3. cadquery-high-level-design → high_level_assembly.py │
│ 4. cadquery-render → read PNGs │
│ 5. ASK USER to confirm decomposition + proportions │
│ 6. iterate until confirmed │
└────────────────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────────────┐
│ PHASE B — BOTTOM-UP (leaves first; one part at a time) │
│ For each part (see build order): │
│ 1. cadquery-part → parts/<name>.py │
│ 2. smoke exec + cadquery-render │
│ 3. cadquery-interference-check (part + mating neighbors) │
│ 4. cadquery-part-detailing │
│ 5. cadquery-render │
│ 6. cadquery-manufacturability │
│ 7. cadquery-render + swap into <group>_assembly.py / assembly.py │
│ Final: global interference → final render → STEP export │
└────────────────────────────────────────────────────────────────────────┘
Phase A — top-down
1. Decompose (orchestrator)
Before any code, produce a decomposition table (also written into common.py as a comment block):
| Function | Sub-assembly | Envelope (mm) | Origin rule | Mates (partner → joint) | OTS? |
|---|
Use function names (drive_shaft, left_bracket) — never part1. If ambiguous, pick the most realistic mechanical split and state assumptions in the table — do not block on questions unless safety-critical dimensions are missing.
2. Invoke cadquery-high-level-design
Hand off the decomposition. It must create common.py and high_level_assembly.py with nested cq.Assembly trees mirroring the final product hierarchy.
3. Render and user gate (non-skippable)
Invoke cadquery-render on high_level_assembly.py. Read all five PNGs. Present decomposition + one screenshot summary.
Ask explicitly: "Is this the right decomposition and proportion? Anything to add, remove, or resize before detailing?"
Do not start Phase B until the user confirms (or gives explicit resize instructions you apply and re-render).
4. Freeze the contract
After confirmation, treat common.py envelope values as frozen unless the user changes them. Phase B parts must not invent new global dimensions.
Phase B — bottom-up
Build order
- Leaves — pins, spacers, fasteners-as-parts, small inserts, vendor envelopes
- Anchors — base plates, hubs, shafts, fixed references
- Structures — housings, brackets, lids that reference anchors
- Sub-assemblies — compose children only after all their parts pass the per-part loop
- Root
assembly.py— last
When a later part invalidates an earlier one, re-enter that part's pass (usually functional or DFM) — do not patch only the assembly file.
Per-part loop (mandatory handoff payload)
For each part, invoke sub-skills with a explicit payload:
Part: <function_name>
File: parts/<function_name>.py
Process: machined | sheet_metal | print | cast
Envelope from common: <dims>
Mating partners:
- <partner>: <joint kind> — this part provides: clearance | pilot | spigot | socket | weld face | snap beam
Interface constants from common: <MATE_* names and values>
B.1 cadquery-part → smoke test
After the part file is written, always run:
cd <project> && python -c "import runpy; ns=runpy.run_path('parts/<name>.py'); assert ns.get('result') is not None; s=ns['result'].val(); bb=s.BoundingBox(); print(bb)"
Bounding box must be non-degenerate (each axis span > 0.1 mm). A part collapsed to a point means wrong origin or empty Workplane — fix before continuing.
B.2 cadquery-render on the part file
Inspect PNGs for missing features and wrong scale.
B.3 cadquery-interference-check (local)
Temporary assembly: this part + direct neighbors (detailed or envelope). Fix unintended clashes before detailing — detailing ribs often make clashes worse.
B.4 cadquery-part-detailing
B.5 cadquery-render — check fillets actually appeared (sharp edge = dropped selector)
B.6 cadquery-manufacturability
For bolted joints: clearance holes on the clamped part, tap pilots on the receiving part — coordinated via common.py hole positions. Never drill only one side of a joint.
B.7 Swap into assembly
Update the relevant *_assembly.py to from parts.<name> import build_<name> and assy.add(build_<name>(), ...). Re-render the sub-assembly, then root when needed.
Prefer Assembly.constrain(...).solve() over hand-tuned cq.Location when two parts share a planar or coaxial interface — see cad-generation.txt Assembly mates section.
Placement and origins (orchestrator enforces)
Document in common.py:
# COORDINATE CONVENTION
# Origin: <where>
# +X / +Y / +Z: <directions>
# Table/floor: Z = ...
Floor-standing parts — build with bottom at Z=0, not centered on Z:
# ✅ bottom on table; top at Z = height
part = cq.Workplane("XY").box(w, d, h, centered=(True, True, False))
# ❌ do NOT also offset by h/2 in Location unless you know the part is Z-centered
Part-local origins — each build_<part>() is modeled in its own frame; sub-assemblies apply loc=. Never bake global assembly offsets into the part file unless that part is the global anchor (e.g. base plate at world origin).
Global checks before STEP
cadquery-interference-checkon fullassembly.py— 0 unintended clashes, 0 unintended gaps on declared flush matescadquery-renderonassembly.py— all parts visible, correct envelope vscommon.py- Code scan: no
rect(centered=False), nocq.selectors.*, no compound boolean tools, top-levelresult =on every file python -cexec onassembly.pysucceeds
STEP export
Never put cq.exporters.export() in generated model files. From project root:
cd <project> && python -c "
import cadquery as cq, runpy
ns = runpy.run_path('assembly.py')
cq.exporters.export(ns['result'], 'assembly.step')
print('exported', 'assembly.step')
"
Hard rules — quick recap
- Top-level
result = ...on every file. - Multi-file layout with
common.py+build_*()— no monolithic assembly geometry. - No
rect(centered=False)— use.center(dx, dy).rect(w, h). - String edge selectors only.
- Single-extrusion boolean tools, one
cut/unionat a time. - No
.translate()on empty Workplanes — useWorkplane(plane, origin=...)or translate after geometry exists. - No reused rotated Workplane for multiple cuts — rebuild each tool.
- No loft/sweep as boolean tools — shell instead.
- Every joint modeled on both sides where applicable.
- Vendor parts: interface geometry in functional pass; procurement comment in DFM pass; use
step.partsor injected STEP when available.
When to stop
Stop when: (a) user confirmed Phase A; (b) every part completed functional → render → local interference → detailing → render → DFM → render, and is swapped into its sub-assembly; (c) global interference clean; (d) five views of assembly.py correct; (e) STEP exports without OCCT errors.
Anti-patterns (reject immediately)
| Symptom | Fix |
|---|---|
All parts in one .py file |
Split per multi-file rules |
| Parts float / gaps at mates | Shared MATE_* in common.py; use constraints or recalc loc from anchor faces |
| Duplicate holes from part + DFM | Functional = interface bore only; DFM = clearance/counterbore/tap |
| Fillets in functional part file | Move to detailing pass |
| Skipped Phase A user confirmation | Stop and render high-level; ask user |
| Render never opened | Always Read PNGs — do not assume pass |
high_level used z = h/2 Location on Z-centered box |
Use centered=(True,True,False) at Z=0 |
| 30+ parts flat on root assembly | Group into ≤6 sub-assemblies |