agents/openai.yaml
interface:
display_name: "CAD"
short_description: "Generate and validate CAD artifacts."
default_prompt: "Use $cad to create, regenerate, inspect, and validate explicit CAD files and selector refs, handing supported outputs to $cad-viewer when available."
LICENSE
MIT License
Copyright (c) 2026 Thompson Labs LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
references/build123d-modeling.md
# build123d modeling patterns
Read this file when writing or repairing build123d Python source.
## Modeling objective
Create a valid STEP-ready BREP model, not a visual mesh. Prefer closed solids, explicit labels, and stable parametric dimensions. Define the `@step` model function returning the STEP-ready shape or labeled compound; the CLI owns output paths (see `step-generation.md`). Name a buildable entry generator `<name>.py` (the marker the viewer and build tools scan for); keep `<name>.py` for helper/library modules that are only imported, not built on their own (see "Entry generators are named `<name>.py`" in `step-generation.md`).
## Design strategy
Decide how the part is constructed before writing geometry code:
- **Choose the construction that makes the spec's dimensions direct parameters.** Profile-driven shapes get one closed sketch plus `extrude`/`revolve`/`sweep`/`loft`; block-and-feature parts get a base solid plus subtractive features. Prefer whichever construction lets the user's controlling dimensions appear as named parameters instead of derived values.
- **Decide part vs assembly before modeling.** Bodies that are separately manufactured, purchased, or movable belong in a labeled assembly (see `positioning.md`); monolithic manufacturing intent gets a single fused solid. Avoid unlabeled compounds of solids — multi-body output without occurrence labels loses traceability in inspection and viewer review.
- **Pick the origin and orientation from the functional datum before sculpting.** Model on the mating interface, mounting plane, or symmetry axis; see `positioning.md` for part-type origin defaults.
- **Order operations so fragile steps come last and failures localize.** Base solid → major additions → subtractive features → shell → through-wall holes → fillets and chamfers last. Fillets are the most failure-prone operation and every boolean invalidates selectors, so postpone them. Structure the source so each feature is a named step — a per-feature function or a distinct intermediate variable — so a failed operation points at exactly one feature and a parameter change touches one obvious place.
- **Overshoot boolean tools.** Extend cutting tools past the faces they enter and exit; for through-cuts, go roughly 1 mm beyond both faces. Coincident or coplanar tool/target faces are a classic kernel failure. Cut repeated or patterned features in one combined operation.
- **Sanity-check proportions before generating.** Compare the expected bounding box against the real-world object, wall thickness against overall size, and feature positions against edges and neighboring features. Order-of-magnitude and collision errors pass geometric validation but fail visual review.
## Topology stack
Think in this order:
```text
Vertex → Edge → Wire → Face → Shell → Solid → Compound
```
For assemblies, use these repo topology terms consistently:
- **Occurrence**: a placed node in the assembly tree. An occurrence has a parent, transform, path, and user-facing role such as `lid` or `m3_screw:front_left`.
- **Shape**: an exported geometry/body inside an occurrence. Shape rows own topology; faces and edges belong to a shape, and the shape belongs to an occurrence.
- **Face/edge**: selectable topology owned by a shape. Do not assume arbitrary faces or edges have persistent intent labels; inspect them by occurrence, shape, ordinal, surface/curve type, and measured geometry.
When inspecting topology, follow `assembly occurrence -> shape/body -> faces -> edges`. Every face/edge row should be traceable through both `occurrenceId` and `shapeId`.
For normal STEP output, return one of:
- a valid `Solid`
- a compound of valid solids
- a labeled assembly compound
Avoid returning loose wires, open faces, or construction surfaces unless the user explicitly requested them.
## Parameters first
Put meaningful dimensions in named variables:
```python
width = 80.0
depth = 50.0
thickness = 6.0
hole_diameter = 4.5
hole_offset_x = 30.0
hole_offset_y = 17.5
```
Avoid burying important numbers inside geometry calls.
## Coordinate system
Declare or comment the convention:
```text
Origin: center of primary part or chosen mating datum
XY: main base/sketch plane
+Z: up/extrusion direction
```
Use `Location`, `Plane`, and `Axis` intentionally. For positioning-sensitive tasks and source-level assembly relationships, read `positioning.md`.
## Builder contexts
Use the context that matches the geometry:
```python
with BuildLine() as path:
...
with BuildSketch() as profile:
...
with BuildPart() as part:
...
```
Typical flow:
```text
curves/paths → sketches/profiles → solids/features → labels → STEP
```
## Selection practices
Avoid fragile topology order when possible. Select by:
- axis or normal
- location or bounding position
- plane grouping
- feature intent
- stable construction plane
- inspected local selector ref for downstream validation
For source operations, prefer robust selectors such as top/bottom by axis or position rather than arbitrary list indexes.
## Assemblies and positioning
For assemblies, keep this file focused on BREP modeling patterns and labels. Use `positioning.md` as the single source of truth for:
- part-local coordinate conventions
- when to use `cadgen.assembly.AssemblyHelper`, build123d joints, or explicit `Location` transforms
- `connect_to()` behavior
- CLI `inspect align` as read-only selector-pair alignment validation
- frame, measure, and positioning report expectations
## Labels and assemblies
Label every exported part and assembly child with native build123d labels. Prefer concise intent labels through `cadgen.assembly` helpers:
```python
from cadgen.assembly import AssemblyHelper, label_shape
asm = AssemblyHelper("electronics_enclosure")
base = asm.add(make_base(), "base")
lid = asm.add(make_lid(), "lid")
boss = label_shape(Cylinder(radius=3.0, height=12.0), "m3_boss", "front_left")
```
Do not prefix labels with topology categories like assembly, component, feature, datum, mate, or hardware. The assembly tree and topology inspection already expose those structural categories. Use labels for the intent topology cannot reliably infer: role, placement, interface, repetition, or mating purpose. Feature labels survive STEP export best when the feature remains a labeled child shape in a `Compound`; boolean-subtracted or fused feature history should be represented by source parameters, named datums, and validation refs instead of assumed persistent feature labels.
Label for inspection:
- Label the root assembly.
- Label every exported part, subassembly/module, and repeated component occurrence.
- Use occurrence labels for assembly role and placement, especially repeated parts: `m3_screw:front_left`, `m3_screw:rear_right`.
- Use shape labels for retained exported geometry/body roles where useful.
- Use feature/datum labels only when that geometry remains exported as a child shape.
- Use named mate datums for source-level positioning intent, then validate the exported STEP topology and occurrence frames.
Occurrence and shape labels are exported through STEP names and surfaced in `STEP_topology` when available. The viewer uses occurrence labels for assembly/tree references and shape labels for shape references. Faces and edges inherit their context from `occurrenceId` and `shapeId`; do not promise persistent face/edge intent labels unless explicit tested support exists.
For repeated parts, keep occurrence labels, transforms, or joint connections explicit and inspect frames/positioning after generation.
## Colour
Two rules, both of which fail silently — no error, just a model that looks wrong.
**Channels are LINEAR RGB, not sRGB.** The renderer converts them to sRGB on the
way to the screen, so `Color(0.5, 0.5, 0.5)` displays as roughly `#BCBCBC`, not
`#808080`. Picking channel values off a hex palette by eye gives a washed-out,
desaturated model. Author with `cadgen.srgb()`, which takes the hex you want to
see:
```python
from cadgen import srgb
body.color = srgb("#2E3742")
glass.color = srgb("#38414D", 0.42) # with alpha
```
**Colour on a group compound is ignored.** Only *leaf* occurrences carry colour
into the model's tree, so a colour set on a `Compound` that has children never
reaches the screen. It does reach the STEP file's XCAF label, which is why this
looks like it worked if you only check the STEP. Colour every leaf.
## Finish
Colour alone cannot tell cast from machined from carbon: those differ in how
they RESPOND to light, and by default every part takes the viewer theme's one
roughness/metalness/clearcoat. A leaf shape may carry a `cad_material` dict to
override those per part; the values ride the tree's occurrence and
the viewer applies them over the theme, so the same model reads differently
under every theme without re-authoring:
```python
housing.cad_material = {"roughness": 0.85, "metalness": 0.2} # as-cast
journal.cad_material = {"roughness": 0.25, "metalness": 0.9} # ground steel
lacquer.cad_material = {"roughness": 0.4, "clearcoat": 1.0, "clearcoatRoughness": 0.1}
window.cad_material = {"opacity": 0.35}
```
Keys: `roughness`, `metalness`, `clearcoat`, `clearcoatRoughness`, `opacity`,
each clamped to 0..1; unknown keys are ignored. Like colour, it belongs on the
LEAF — a group compound's `cad_material` reaches nothing — and it is a
presentation hint only: STEP has no channel for it, so it lives in the package,
not the file.
## Rotating a plane
`Plane.rotated()` composes its matrix in **WORLD axes, not the plane's own**.
On a plane whose axes are not the global ones this is the single most expensive
trap in the library, because the result is a valid solid of the wrong shape.
For a spanwise aerofoil section — `x_dir=(-1,0,0)`, `z_dir=(0,1,0)`, i.e. local
+x rearward and the normal along +Y — `plane.rotated((0, 0, twist))` reads like
a pitch and is actually a **yaw about world Z**. Measured on a 200 mm chord: a
20 deg "twist" put the trailing edge at `(812.0, -68.4, 99.4)` when it should
be at `(812.1, 0.0, 168.4)`. The section slid 68 mm sideways out of its own
spanwise station and rose nothing.
Nothing downstream catches it. The loft succeeds, the solid is closed,
watertight and free of self-intersections, and `cadgen step inspect refs --facts`
passes it. Only looking at a render finds it.
Build the frame from explicit direction vectors instead:
```python
# incidence about the span axis, then yaw the whole frame
t, s = math.radians(twist_deg), math.radians(sweep_deg)
x_dir = Vector(-math.cos(t) * math.cos(s), -math.cos(t) * math.sin(s), math.sin(t))
normal = Vector(-math.sin(s), math.cos(s), 0.0)
plane = Plane(origin=Vector(*origin), x_dir=x_dir, z_dir=normal)
```
The same applies to rolling a section about a swept member's own axis: use a
Rodrigues rotation about that axis rather than `Plane.rotated()`.
## Multi-section lofts match sections BY INDEX
A loft interpolates its sections point index by point index. If you sample each
station at fractions of THAT station's own width, a feature — a crest, a
silhouette edge — sits at a different index at every station, and the surface
twists between them to reconcile them. The result is valid, watertight,
bilaterally symmetric, passes `inspect validate`, and renders as **crumpled
foil** over every square metre. Nothing reports it; only a render finds it.
Sample on **rails**: compute the lateral position of each feature line per
station and allocate a fixed number of points to each rail-to-rail band, so
index *i* means the same feature everywhere. Cluster samples toward the rails —
that is where curvature is worst, so even spacing inside a band leaves the
sharpest part of the curve least resolved.
Two more ways a control curve silently ruins a lofted surface:
- **`smoothstep` between control points makes a staircase.**
`lerp(v0, v1, smoothstep(x0, x1, x))` has zero derivative at BOTH ends of every
interval, so the curve is flat at each control point and steep between them.
Lofting through such curves puts a crease at every knot. Use a monotone cubic
(PCHIP) instead.
- **Measurement noise becomes surface ripple.** Station data traced off a scan
carries ~a pixel of noise; a monotone interpolant reproduces it exactly and the
loft turns it into visible waves. Smooth the control curve before lofting.
## Blending volumes: a closed lobe that ends inside the body is a cliff
When sections are built by smooth-max/min over component volumes, any closed
convex profile meets its own silhouette on a **vertical tangent**. Where such a
lobe closes *inside* the body — against a neighbouring shelf or lobe — you get a
near-vertical wall no blend width and no sample density can round off. Extra
sampling does not help: the corner is in the function, not the sampling.
- Widen the lobe until it OVERLAPS its neighbour and cut the real feature back in
afterwards, rather than letting it close between them.
- Give a feature that needs its own width its own lobe. One half-width cannot
serve both a wide fuselage and a narrow canopy.
- Prefer a **compact-support polynomial** smooth-max to the softplus/log-sum-exp
form: softplus perturbs the surface everywhere and its curvature is unbounded
as the blend narrows. Use the **cubic** (`h**3`) form, not the quadratic
(`h*(1-h)`) one — the quadratic is only C1, so curvature JUMPS at the edge of
the blend band, and a curvature jump on a specular surface draws a visible
line.
## Validity is not positive volume
`Shape.is_valid` (and `BRepCheck_Analyzer`) can return **True for a shell with a
large negative volume** — an inverted orientation. Such a body exports and
renders as a hole in the world. Check both:
```python
def is_valid_shape(shape):
return (shape is not None
and BRepCheck_Analyzer(shape.wrapped).IsValid()
and shape.volume > 0.0)
```
Related: a boolean can leave a body that is geometrically right but
topologically invalid — correct bounds and volume, one bad face. It survives
until the next boolean, which then fails with `Null TopoDS_Shape object` from a
call nowhere near the cause. `ShapeFix_Shape` repairs many of these; gate every
boolean result rather than trusting the last operation.
`cadgen step inspect validate` runs both of these gates plus closure and
self-intersection over every occurrence, so this does not have to be hand-rolled
per model. Note it measures volume **per solid**: an inverted member inside a
compound cancels against a sound one, so anything reading a compound's aggregate
volume sees nothing wrong.
## A revolve puts its seam at +X
A 360-degree `revolve` leaves a seam edge where its profile started, and
sketching on `Plane.XZ` places that seam at **+X**. If the presentation camera
looks down +X, every revolved casting renders with a thin panel line down its
visible face — on parts whose whole point is a smooth, sealed surface.
This is not limited to `revolve`: a plain `Cylinder` primitive seams at +X too,
verified with a marker probe. Any large smooth camera-facing cylinder is
affected.
Rotate the finished body about Z so the seam lands away from the camera. Two
cautions:
- A body carrying discrete features (a bolt ring, a stud circle) must be rotated
by a whole number of feature pitches, or left alone and its *prototype*
rotated instead — `bolt_ring`-style helpers only translate their prototype, so
seam-hiding the prototype does not move the ring.
- A body offset from the origin must be rotated about **its own** axis: build it
at the origin, rotate, then translate. Rotating in place about global Z flies
it across the model.
Prove the fix with two renders — the seam absent from the camera face **and**
present on the far side. Without the second render you cannot tell a hidden seam
from one that was never visible at that angle.
## Fillet retry ladders degrade silently
The `pipe()`-style retry ladder (`[bend, .7, .5, .3, 20]` around
`FilletPolyline`) exists for a good reason: one oversized corner otherwise kills
an entire build with `BRep_API: command not done`. But it converts a hard
failure into an invisible cosmetic regression.
Where a profile cannot accept the nominal radius, the ladder silently falls back
— a 6 mm rim fillet became ~2 mm on a 660 mm-diameter flange, which tessellates
as a visible sawtooth. The build reports success; only a render cropped to ~5x
shows it.
Do not rely on the ladder for cosmetic radii. Reshape the profile so the
intended radius genuinely fits (a knife-edged wafer cannot take any fillet;
merge it into its neighbour), then verify by cropping the render.
## Multi-tool booleans: one list operation, internally disjoint batches
Never accumulate boolean tools pairwise — `body - a - b - c` re-runs the whole
intersection network per step and decays O(n²). Pass every tool in one list
operand: `body - [a, b, c, ...]`.
Two caveats, both measured:
- **Tools that overlap each other deep below the surface are pathological.**
~200 shallow spherical dimples cut with full spheres (radii ~15 mm for
0.02 mm-deep stamps) ran >40 CPU-minutes with zero output; pre-clipping each
stamp to a small disjoint "lens cap" (`Sphere & Cylinder` prototype,
translated copies) cut the same field in 0.69 s. Keep tools small and
mutually disjoint.
- **A single multi-tool cut whose tools overlap each other can emit wrong
results.** A bore cylinder crossing a stack of thin ring cutters returned
5 solids: the body, the bore's uncut PLUG kept as a detached solid, and
knife-edge slivers. Every tool was individually valid; splitting the same
tools into two staged subtracts (functional cuts, then finishing cuts)
yielded one clean solid. Batch tool FAMILIES so each batch is
internally disjoint-ish — still list-based, never pairwise.
## Near-tangent booleans silently drop material
Intersecting or subtracting nearly tangent surfaces (a huge shallow sphere
kissing a small revolve, a flat dome tool grazing a face) can succeed with
exit 0 and a validate-clean result while half a tool's material was simply not
removed — or a stray disjoint sliver is left floating inside the part. Only
visual review catches it. Build shallow domes as a single revolved profile
(`RadiusArc` in the section) instead of near-tangent boolean stacks; it is
also crisper.
## Do not 3D-chamfer tangent chains or multi-arc outlines
OCC `chamfer`/`fillet` on edges that belong to a tangent chain (a domed face
meeting cap cylinders) or to a multi-arc "blob" outline behaves three ways
depending only on exact dimensions: silent failure, minutes of CPU churn per
attempt, or an **uncatchable SIGSEGV** that kills the whole build. Chamfering
edges NEXT TO already-beveled arcs can also hard-crash. Retry ladders multiply
the churn and hide the degradation.
Bake the bevel into construction instead: put it in the extruded/lofted
SECTION profile, or build the body straight-walled to `z_top - w` and cap it
with `extrude(..., taper=45)` (or per-arc `Cone` caps when the draft prism
itself fails). Constructive bevels also survive later booleans, which
chamfered edges often do not.
## 2D sketch algebra decays; winding decides extrude direction
Chained 2D unions are fragile in three stacked ways, all silent:
- `Circle + Circle` returns a fused `Face`, and the next `Face + Polygon`
falls into raw shape fuse returning an unregularized face pile; once any
step yields a `ShapeList`, later `+` is Python list concatenation, not
geometry. Build each profile as ONE multi-operand fuse:
`first + [rest...]`.
- A CLOCKWISE-wound `Polygon` fuses as a reversed face: the union "succeeds"
but shatters into mixed-normal fragments and `extrude()` runs along the
reversed normals — solids appear mirrored below the plane. Wind every
polygon CCW. **Mirroring a point list reverses its winding**: mirror with
`[(-y, z) for y, z in reversed(pts)]`, or the extrude silently runs the
other way and the cutter lands off the part.
- `ShapeList & Sketch` used as a regularizing clip returns an EMPTY list with
no error, and the following extrude quietly produces a zero-volume part.
Apply the `& clip` intersection exactly once, LAST, on the single fused
profile.
## `align=(None, None, None)` is the raw OCC datum, not "centered"
`Cylinder`/`Cone` with `align=(None, None, None)` sit base-at-z=0 (XY
centered); `Box` sits with its CORNER at the origin. Code written assuming
"None means centered" produces silently wrong geometry — off-center slots,
inverted countersinks, cutters that remove nothing because they sit entirely
above the surface. Two independent modules shipped defects from this exact
assumption. Default alignment IS centered; reserve `align=None` for when the
raw datum is genuinely wanted.
## Place with `Location * shape` or `.moved()`, never `.located()`
Two independent reasons, both silent.
**`.located()` SETS the placement; `.moved()` composes with it.**
`Shape.rotate()` returns a rotated copy. Placing that copy with
`.located(Location(pos))` throws the rotation away: `located` assigns an
ABSOLUTE location, so what lands at `pos` is the ORIGINAL orientation.
`.moved()` and the operator form compose.
```python
box = Solid.make_box(1, 1, 1) # x[0,1]
r = box.rotate(Axis.Z, 90) # x[-1,0] rotation applied
r.located(Location((5, 0, 0))) # x[5,6] rotation DISCARDED
r.moved(Location((5, 0, 0))) # x[4,5] rotation kept
Location((5, 0, 0)) * r # x[4,5] the same, as an operator
Pos(5, 0, 0) * Rot(0, 0, 90) * box # x[4,5] position and rotation, composed
```
Nothing raises, and the bounding box moves the distance you asked for, so the
result looks placed. Verified on build123d 0.11.1.
The failure mode is a sweep that reads as physics. Placing a part with
`.rotate(...).located(...)` inside a loop over angles feeds `intersect()` the
same unrotated shape every iteration, so a gear-mesh collision check returns an
identical volume to 15 significant digits at every phase — a flat, plausible
curve rather than an error.
**`.located()` deep-copies the geometry.** In an assembly body, a child model
placed with `.moved()` or `Location * child` keeps its identity, so the
parent's result LINKS to the child's tree (stored once, shared by every
parent). `.located()` copies the underlying shape, which makes the parent own
a duplicate of the child's geometry as its own component — the file still
builds, the dependency is still tracked, but the link is gone and the
component is written again. Reach for `.moved()` or the operator form; there
is no case that needs `.located()`.
## Dense periodic spline profiles: kernel ops to avoid
On faces bounded by one periodic `Spline` fit through hundreds of samples,
several kernel operations fail or corrupt (verified on build123d 0.10 /
OCP 7.9): `extrude(face, taper=...)` throws `BRepFill_TrimSurfaceTool:
incoherent intersection`; kernel wire `offset` returns Null for some inward
deltas; fusing two valid solids that share a coincident spline-bounded planar
face can return an EMPTY result; and a ruled loft to an inward offset is
analyzer-invalid where the outer wire's corner radius is smaller than the
offset. Compute offsets NUMERICALLY on the sample loop (normal offset, prune
points closer than |delta| to the source polyline, resample, smooth) and build
beveled bodies as one multi-section ruled loft so no coincident-face fuse
exists.
## Gate boolean results with the BOP check, not volume
`result.volume > 0` and even `BRepCheck_Analyzer.IsValid()` both accept
chamfer and V-groove-cut results whose skinny faces are BOP-faulty
(`BOPAlgo_SelfIntersect`, `BOPAlgo_TooSmallEdge`). The failure then surfaces
only in `cadgen step inspect validate` (`selfIntersecting`), with no pointer to
the causing operation. After tangency-prone cuts and chamfers on wavy
outlines, gate with the same check validation uses — `BRepAlgoAPI_Check` —
and step the operation down or skip it when the check fails.
Related wrap trap: re-wrapping a bare `Solid` as `Part(solid.wrapped)` yields
a shape whose `.volume` is 0 (build123d 0.10), so volume-based guards silently
discard real geometry. Use the `Solid` directly as a compound child (Shape
carries `label`/`color`), or fuse before measuring.
## Common failure modes
- Fillet radius larger than local edge geometry.
- Open sketch profile produces invalid or missing face.
- A loft whose SECTION WIRE self-intersects. `make_face` accepts a
self-intersecting periodic spline and reports a valid, positive-area face, so
each station looks fine in isolation; the loft then fails on whichever
adjacent pair is worst. Bisect by lofting adjacent pairs to find the station.
Common cause: two points straddling a crease offset along the corner's
TANGENT lines rather than placed on the curve, so the outline doubles back.
- Smooth `loft()` failing with `BRep_API: command not done` even though every
section is individually valid and they all share one edge count. Try
`loft(..., ruled=True)`; with densely spaced sections the result is visually
equivalent.
- `solid += helper()` where the helper returns a *list*: the accumulator becomes
a `ShapeList`, and the failure surfaces much later as an anytree
`Cannot add non-node object` from inside `Compound(children=...)`.
- Face selector changes after a boolean or fillet.
- Part origin is arbitrary and later alignment checks become ambiguous.
- Source-level joints are treated as if they were persistent STEP constraints rather than one-time source placement operations.
- Joint labels are missing, duplicated, or attached to the wrong local datum.
- `.connect_to()` fixes the wrong side of the relationship, moving the part intended to remain fixed.
Use `repair-loop.md` when generation or validation fails.
references/cad-brief.md
# CAD brief
Read this file when converting a user's request — prose, reference images, technical drawings, or a combination — into a CAD brief. The brief is an internal note-taking scaffold; do not ask the user to fill it out, and do not require the user to provide JSON. If the user supplied JSON voluntarily, extract the same information but continue the workflow in prose notes and build123d source.
## Goal
Convert the request into an actionable modeling brief before writing source or running tools. Every input modality funnels into the same brief; the downstream workflow does not change.
The brief should answer:
- What is being modeled, and is it a part, assembly, modification, inspection task, or secondary output request?
- What dimensions and units are specified, and which missing dimensions are inferable?
- Which features are required?
- Which faces, axes, origins, joints, or interfaces control positioning?
- What output files are requested?
- What must be validated before success is reported?
When inputs conflict, dimensioned sources win over image proportions. When two dimensioned sources conflict — prose says one value, a drawing callout says another — flag the conflict instead of silently choosing.
## Reference images
An image without stated dimensions is design intent, not a spec:
- Establish scale from one stated dimension or a known object in frame; if neither exists and fit matters, that is the one clarification question to ask.
- Estimate remaining proportions from the image and record them as assumptions like any other inferred value.
- Distinguish reproduction ("model this part") from inspiration ("something like this") in the brief; reproduction raises fidelity expectations, inspiration leaves freedom.
- For reproduction, plan a snapshot from the reference image's viewpoint and compare it against the image during visual review.
## Technical drawings
A drawing is a dimensioned contract. Extract it systematically:
- Read the title block and notes first: units, projection convention, revision, disclaimers.
- Identify which view is which — front/top/side, sections, details, iso — and which model axes each maps to before extracting numbers. Trust callouts and view labels, not layout conventions. Section views are the source of truth for internal features: bores, counterbore and blind-hole depths, wall sections.
- Convert every dimension callout into a named parameter and a validation target. Multiplicity (`4X`), `TYP.`, and thread/counterbore/countersink callouts expand into features plus checks.
- Never scale undimensioned geometry off the image. Derive it from stated dimensions when constrained; otherwise assume and report.
- Cross-check features across views; when views disagree, prefer the dimensioned view and flag the conflict.
- Success for a drawing-driven model: every drawing dimension is either verified by `measure`/`refs` after generation or explicitly reported as not verified.
## Brief format
Use concise Markdown notes, not a user-facing structured schema:
```text
CAD brief:
- Model: <part or assembly name>
- Task type: <new part, assembly, modification, inspection, secondary output>
- Inputs: <reference images or drawing views used; omit when prose-only>
- Units: <explicit or assumed>
- Coordinate convention: <origin, base plane, up axis>
- Overall dimensions: <width/depth/height or equivalent>
- Functional features: <holes, slots, ribs, bosses, pockets, shells, text, etc.>
- Manufacturing assumptions: <only when relevant>
- Positioning/mating: <interfaces, datums, child placements, joints, alignment rules>
- Paths: <generator .py, STEP target, secondary outputs if requested>
- Validation targets: <bbox, solid count, labels, spec-driven measurements, refs>
- Assumptions: <only meaningful inferred choices>
```
## Example: simple part
User says:
```text
Make a 100 mm by 60 mm by 6 mm mounting plate with rounded corners, four M4 clearance holes 10 mm in from the corners, and a 20 by 12 mm rectangular cutout in the center.
```
Agent brief:
```text
CAD brief:
- Model: mounting_plate, single STEP part.
- Units: millimeters.
- Origin: center of plate; base plane XY; +Z is thickness direction.
- Body: rounded rectangular plate, 100 × 60 × 6 mm.
- Corner radius: not specified; assume 3 mm.
- Holes: four 4.5 mm M4 clearance through-holes, 10 mm in from each corner.
- Cutout: centered rectangular through-cut, 20 × 12 mm.
- Validation: one positive-volume solid, bbox 100 × 60 × 6 mm, four holes, one center cutout, label mounting_plate.
```
## Example: assembly
User says:
```text
Design a two-piece enclosure, 120 by 80 by 35 mm, with a lid that sits on top and four screw bosses aligned between base and lid.
```
Agent brief:
```text
CAD brief:
- Model: enclosure assembly with base and lid.
- Units: millimeters.
- Assembly origin: center of enclosure footprint; +Z upward.
- Base: hollow lower shell, exterior 120 × 80 mm footprint; height derived from total height minus lid thickness.
- Lid: separate plate on top; assume 3 mm lid thickness unless user gave another value.
- Bosses: four aligned screw bosses; assume M3 unless unspecified dimensions make this unsafe.
- Positioning: base top face and lid bottom face are mating datums; screw axes must align; native build123d joints may be used if they clarify reusable mount points or motion.
- Validation: labeled base and lid children, bbox near 120 x 80 x 35 mm, aligned hole/boss axes.
```
## Clarification policy
Ask one focused question only when the missing information affects fit, safety, compliance, or makes the part impossible to model. Otherwise proceed with assumptions and report them.
Ask when:
- No dimensions are provided for a physical object, and no scale reference exists in the supplied images.
- A mating interface is described but the mating geometry is unspecified.
- The part is safety-critical, load-bearing, pressure-bearing, medical, or compliance-bound.
- The requested output depends on an absent source file or missing imported geometry.
Do not ask when:
- A default clearance hole standard is sufficient.
- A cosmetic fillet radius can be safely assumed.
- Origin/orientation can be chosen and reported.
- The user is asking for a conceptual first-pass CAD model.
## Success criteria
A brief is ready for modeling when it contains enough information to define:
- source file path and STEP target path
- units and local coordinate system
- named parameters
- feature plan and labels
- expected bounding box or key measurements
references/inspection-and-validation.md
# Inspection and validation
Read this file for every generated STEP artifact and whenever the user asks for geometry facts, references, dimensions, mating, diffing, or frame inspection.
## Principle
Deterministic geometry checks decide pass/fail; mandatory snapshot review (see `snapshot-review.md`) catches semantic errors the deterministic checks did not encode. Scale the deterministic checks to the user's spec: every dimension, clearance, or relationship the user specified — including dimensions taken from a technical drawing — must be verified with `measure`, `align`, or `frame`. The facts/planes/positioning baseline runs for every generated artifact regardless of spec.
## Tool
The launcher lives in the CAD skill directory:
```bash
cadgen step inspect {refs|diff|frame|measure|align} ...
```
Targets take native path semantics, like every other cadgen path argument: a relative target resolves against the command cwd, an absolute target works from anywhere, and `~` expands. A target naming a file that does not exist reports file-not-found for that path. Prefer cwd-relative targets from the workspace that owns the artifact anyway — reports name a target by its cwd-relative path when it is inside the cwd, and by its bare file name when it is not, so cwd-relative targets read better in a report. Common data-output flags: `--format json|text` (default is machine-readable), `--quiet`, `--verbose`.
Accepted target forms:
```text
path/to/document.step
path/to/document.stp
```
Targets are documents, spelled with their extension: a bare `<name>` and a `.py`
model script are refused (run `python <model>.py`, then inspect the STEP it
wrote). A door never opens the scripts beside a document to learn which one
wrote it.
Selector-backed queries (`refs --facts`, planes, measures) resolve from the document's tree in the store — its per-component `.surf` objects — on demand; a document with no tree yet is compiled from its bytes first, generated or imported alike. There is no separate topology sidecar to build or invalidate, and a document is never refused for being behind its script.
Selector refs are local to the STEP/CAD entry target passed to the command:
```text
#o1.2
#o1.2.f1
#f1
```
Pass selector refs as `#...` tokens. The STEP/CAD file path or entry target is a separate CLI argument.
An occurrence ref may name a **subassembly** as well as a part — the same `#o1.4` the CAD
Viewer copies, `snapshot --focus` takes, and a kinematics mate poses. A subassembly owns no
geometry of its own, so it resolves as the parts beneath it: `refs` reports one entry per
part (each tagged `fromGroup`), and `measure`/`align` use the branch's combined extent.
`frame` answers for the branch — its name from the instance tree, plus the extent and
center of its parts; a subassembly has no transform of its own, because group placement is
baked into each part's absolute transform. Counts (`occurrenceCount`, `refs --facts`) stay
leaf-based. An occurrence ref that names nothing lists what the document does have at that
depth.
### File-prefixed refs (the CAD Viewer copy format)
A ref copied from the CAD Viewer carries the file it came from, so it stays meaningful in a
prompt that spans several files:
```text
bracket#o1.2.f1 the generator src/bracket.py
imported_housing.step#o1.3 a raw STEP, STEP/imported/imported_housing.step
mounting_plate.stl# a whole mesh file
```
The prefix is the **shortest path suffix that names exactly one file**, plus as many leading
directories as it takes to be unique. A prefix with no selectors after the `#` names the whole
file.
A `.py` generator shows as a bare stem, because generators are what you normally work in
and the common case deserves the shortest name. Everything else keeps its suffix:
| File | Prefix |
| --- | --- |
| `bracket.py` | `bracket` |
| `bracket.step`, `bracket.stp` | `bracket.step`, `bracket.stp` |
| `plate.stl`, `plate.3mf`, `plate.glb`, `outline.dxf` | unchanged |
Keeping those suffixes is what makes the stripping safe: `bracket` (the generator) stays
distinct from `bracket.step` (its export) and from `bracket.stl` (a mesh of it).
**These CLIs do not resolve prefixes. You do.** When you receive a file-prefixed ref:
1. Split it at the first `#`. The left side is the file prefix; the right side is the ref.
2. Resolve the prefix to a real path. A bare stem is **not** a literal path suffix, so expand
it before searching:
- `<name>` with no extension → the model script `<name>.py`; its DOCUMENT (the sibling
`<name>.step` by default, or the decorator's `out=` target) is what the commands take
- anything carrying a suffix (`.step`, `.stp`, `.stl`, `.3mf`, `.glb`, `.dxf`) → use as-is
Match on **segment boundaries**, so `plate.stl` names `STL/plate.stl` and never
`STL/mounting_plate.stl`.
3. Pass the resolved document as the entry/input argument and the `#...` part as the ref,
exactly as you would for a bare ref.
```bash
# received: bracket#o1.2.f1 -> expand the bare stem, then search
git ls-files '*/bracket.py'
cadgen step inspect refs STEP/bracket.step '#o1.2.f1'
```
If the search returns more than one file the prefix was ambiguous — ask rather than guess; the
Viewer only emits prefixes that were unique when it copied them.
Passing the prefixed ref through unsplit also works **when the prefix names the file the
command already targets** — the CLI strips it, and it accepts every spelling of that file
(`bracket`, `bracket.py`, `bracket.step`). A prefix naming a *different* file is a hard
error, never ignored: silently inspecting the file the command was pointed at would produce a
confident answer about geometry nobody asked about.
```text
ref 'other_part#o1.2' names file 'other_part' but this command targets
'STEP/bracket'; pass the file as the entry argument and the '#...' part as the ref
```
Bare `#...` refs are unchanged and work everywhere they always did.
### Referencing a part by its label
A part's build123d label can stand in for its occurrence id anywhere a ref is accepted:
```text
#eye_shank the part labelled eye_shank
#eye_shank.f45 a face on it
#eye_shank.f45,f46 two faces on it -- the label carries forward like an occurrence id
```
Numeric refs are unchanged and always work; labels are an additional spelling, not a
replacement. `snapshot --mode list` shows each part's `name`, and `inspect refs` reports the
exact ref to paste as `labelRef`.
A label may contain letters, digits, `_` and `:`, and may not start with a digit. Parts whose
label cannot be spelled that way, or which collides with the numeric grammar (`f12`, `o1`,
`m2`), are addressable by their numeric ref only.
When several parts share a label -- two wheels, one `cast_rim:5spoke` -- each gets a numbered
ref in tree order and the bare label refuses to resolve rather than guessing:
```text
$ cadgen step snapshot motorbike.step --focus '#cast_rim:5spoke'
selection.focus label 'cast_rim:5spoke' matches 2 occurrences;
use one of: #cast_rim:5spoke_1 (o1.7.2), #cast_rim:5spoke_2 (o1.14.2)
```
## Validation sequence
1. Generation completed and the STEP/STP file exists.
2. `refs --facts --planes --positioning` confirms scale, labels, major planes, and placement-ready references. Run this for every generated artifact.
3. `validate` confirms the geometry is sound: valid topology, closed shells, no self-intersection, and positive volume on every solid. Run this for every generated artifact.
4. Spec-driven checks: `measure` for every user-specified dimension, offset, or clearance; `align` for interfaces that should be flush or centered; `frame` for orientation and occurrence-placement expectations; `diff` for modifications that could affect unrelated geometry.
5. Snapshot the primary STEP/STP per `snapshot-review.md`, then convert every visual concern into a deterministic geometry check before it becomes a validation claim.
### `refs --facts` "ok" is not a geometry claim
`refs --facts` reports counts, bounds, labels and references. Its `ok` field is
a command-success flag: it is true when every requested ref resolved, and it
says nothing about whether the geometry is sound. A five-face open box reports
`"ok": true` with `"faceCount": 5`, and a solid with inverted orientation —
which renders as a hole in the world — reports `"ok": true` as well.
Use `validate` for that question:
```bash
cadgen step inspect validate models/part/part.step
cadgen step inspect validate models/part/part.step --refs o1.2 # one subassembly
cadgen step inspect validate models/panel/panel.step --allow-open # surfaces intended
cadgen step inspect validate models/rig/rig.step --out validate.json # keep a partial on a kill
```
It reports any of `invalidTopology`, `openShell`, `nonPositiveVolume`,
`noSolid`, `selfIntersecting`, and exits non-zero when any occurrence fails.
Each `parts` entry is one finding on one shape: `ref`/`name` is the placement
the checks ran on, and `occurrences` lists every placement the finding applies
to (`failureCount` counts occurrences, `prototypeCount` unique shapes).
Two subtleties worth knowing. `BRepCheck_Analyzer` returns **true** for a
reversed solid, so topological validity alone cannot catch an inverted body —
only the sign of the volume can. And volume is measured per solid, never
aggregated: a `+1000` and a `-1000` inside one compound sum to zero, so any
check reading a compound's total volume sees nothing wrong.
Large assemblies: a part placed a hundred times is ONE shape with a hundred
locations, so topology, closure, solid presence and volume are checked once per
unique shape, in parallel across a process pool (`CADGEN_VALIDATE_WORKERS`
sizes it; `1` runs in-process). The self-intersection test is numeric and can
differ by placement — the same bolt has failed at 15° and 30° of tilt and passed
upright — so by default it runs once per shape at its first placement and the
report says so (`"selfIntersectionCheck": "first-placement"`). Pass
`--every-placement` to run it on every copy (a `selfIntersecting` entry then
lists exactly the placements that failed), or `--skip-self-intersection` to drop
the test when it dominates runtime. Progress paints on stderr per shape;
`--out PATH` also writes the report after every shape with `"partial": true`
until the run completes, so a run that is killed (out of memory, a lost daemon
worker) leaves the findings it reached.
`validate` and `interfere` measure the document ON DISK: it is loaded as
written and runs no Python, even when its script has changed since — a door
never rebuilds a model. Rerun the script first when you want the new geometry
measured. A document the store has never seen (one edited or written by another
tool) is compiled from its bytes on demand, like an import.
### `interfere`: do two parts occupy the same space?
```bash
cadgen step inspect interfere STEP/arm.step --tolerance 0.01
cadgen step inspect interfere STEP/arm.step --refs o1.7 # inside one subassembly
cadgen step inspect interfere STEP/arm.step --refs o1.3,o1.9 # two named parts, as wholes
```
`interfere` intersects every candidate pair of solids (a world-bbox reject runs
first) and reports the pairs whose common volume exceeds `--tolerance` in mm^3.
Touching faces yield hairline slivers, so the default is 1 mm^3; go lower for
small parts.
The unit of the verdict is the **part**: a direct component of the document
root, or of the ref you name with `--refs` (the deepest common ancestor when you
name several). A purchased servo arrives as a sub-assembly whose motor sits
inside its case by construction, and a weldment is several solids in one
product — bodies of one part overlap and always will, and a STEP document
cannot tell a vendor sub-assembly from one you authored. So overlaps between
bodies of the same part are still computed but reported separately, as
`intraPartOverlaps` in `--json` and a per-part summary in text; they never
fail the check. `clashes` — the ones that fail it — are between two different
parts. To test a part's own bodies against each other, name that part alone:
`--refs o1.18`.
Fewer than two bodies, or all bodies in one part, is `INCONCLUSIVE` with
`ok:false`, not a pass: nothing that could fail was tested.
## Reference discovery
Compact facts and planes:
```bash
cadgen step inspect refs path/to/model.step \
--facts --planes --positioning
```
Detailed selector inspection:
```bash
cadgen step inspect refs path/to/model.step '#selector' \
--detail --positioning
```
Topology enumeration, only when needed:
```bash
cadgen step inspect refs path/to/model.step --topology
```
Plane options:
```bash
--plane-coordinate-tolerance FLOAT
--plane-min-area-ratio FLOAT
--plane-limit INT
```
Use lower plane limits and compact facts for normal validation. Use topology enumeration only for selector discovery, complex debugging, or when a feature cannot be verified through facts/planes/measurements; it can be expensive on large models.
## Measurement checks
Use `measure` for bounding distances, clearances, offsets, part spacing, plate thickness, hole-to-face distances, and alignment verification.
```bash
cadgen step inspect measure path/to/model.step \
--from '#selector_a' \
--to '#selector_b' \
--axis x
```
Axis may be inferred when possible, but specify `x`, `y`, or `z` for deterministic checks.
## Alignment checks
Use `align` when two exported STEP references should be flush or centered. It returns a translation delta between the selected refs; apply any required correction in the build123d source (see `positioning.md`), regenerate, and re-inspect.
```bash
cadgen step inspect align path/to/assembly.step \
--moving '#moving_selector' \
--target '#target_selector' \
--mode flush \
--axis z
```
## Frame inspection
Use `frame` to validate occurrence transforms and selected-reference world frames:
```bash
cadgen step inspect frame path/to/model.step '#selector'
```
Frame output is useful for assemblies, part-local-to-world conversion, and placement debugging.
## Diff checks
For modification tasks, compare before and after artifacts:
```bash
cadgen step inspect diff path/to/before.step path/to/after.step --planes
```
Use diff when a repair, feature addition, or source edit could affect unrelated geometry.
## Validation report content
Report only checks that were actually run or directly supported by tool output. If an important selector was inspected, return the local selector ref beside the owning CAD Viewer link.
Use this structure:
```text
Validation:
- STEP generation: passed/partial/failed
- Solids/assembly: <counts and labels>
- Bounding box: <dimensions and units>
- Major planes/refs: <summary>
- Positioning: <frame/measure/align results if relevant>
- Feature checks: <holes, cutouts, bosses, etc.>
- Visual review: `$cad-viewer` viewer link returned; CAD `cadgen step snapshot` PNG included or skipped with reason; follow-up geometry checks for any visual findings
```
Do not claim:
- structural safety
- process certification
- tolerance compliance
- manufacturability beyond geometric plausibility
unless the relevant analysis or manufacturing data was explicitly performed.
references/kinematics.md
# CAD kinematics and animation
Read this file when the user asks to articulate, pose, or animate a STEP
model, or when designing or reviewing mates, couplings, pose presets, posed
exports, or animation clips.
There are THREE systems with different lifecycles, deliberately independent:
- **Geometry** is the module's constants and the factory the parameterless
model calls with them (`WIDTH = 10.0` … `return _bracket(WIDTH)`). Changing
one re-runs Python and rebuilds the outputs. They are not live in the viewer.
- **Kinematics** is typed mates declared as PURE DATA via `kinematics=` on
the export decorators. It drives the viewer's pose sliders — no rebuild, no
Python at render time — and never moves the geometry a model writes. It
lives in the model's sidecar (`<name>.step.json`, written beside the
artifact), the one thing a sidecar is written for.
- **Animation** is choreography in the RENDER MODULE beside the document:
`STEP/<name>.step.js`, next to `<name>.step` and `<name>.step.json`. It is
authored and committed, discovered by name, loaded by the viewer and the
snapshot door, and read by NO build — no decorator names it, the sidecar
carries no copy, the gate has no clause for it. It targets occurrences
directly and knows nothing about mates. Editing it is a reload in the
viewer, never a rebuild; editing kinematics never changes the tree either,
but it does rewrite the sidecar, so a kinematics edit is a (cheap) run.
## Kinematics: typed mates
Kinematics is the ONE thing a model writes a sidecar for, and it never moves
geometry: the declaration describes how the written tree articulates, the
viewer poses it at render time. A model that declares none has no sidecar.
One `kinematics=` dict, closed keys `mates` / `couplings` / `poses`, on any of
`@step`/`@stl`/`@glb`/`@threemf`. Each decorator's declaration stands alone
(share a module-level dict; there is no cross-decorator inheritance).
```python
import cadgen
from cadgen import step
from cadgen import build123d as bd
KINEMATICS = {
"mates": [
cadgen.revolute("elbow", parent="#upper_arm", child="#forearm",
axis="#forearm.pivot_bore", limits=(0, 150)),
cadgen.slider("extend", parent="#rail", child="#carriage",
axis="#rail.f2", limits=(0, 80)),
cadgen.cylindrical("lead", parent="#housing", child="#screw",
axis="#screw.f1",
limits={"turn": (0, 3600), "travel": (0, 40)}),
cadgen.fastened("mount", parent="#carriage", child="#bracket"),
],
"couplings": [cadgen.couple("curl", {"mcp": 50, "pip": 70, "dip": 40})],
"poses": {"open": {"jaw": 40}, "closed": {"jaw": 0}},
}
@step(out="../STEP/arm.step", kinematics=KINEMATICS)
def arm(): ...
if __name__ == "__main__":
arm()
```
- **Mate kinds**: `revolute` (degrees about an axis), `slider` (model units
along it), `cylindrical` (sub-DOFs `<name>.turn` and `<name>.travel` about
one axis), `fastened` (0-DOF rigid attachment — needed exactly when
occurrences are SIBLINGS in the instance tree, like a pin that must orbit
with its carrier; instance-tree children ride for free).
- **`parent`/`child`** are occurrence refs: `#`-prefixed labels (canonical —
label parts with `cadgen.label_shape`) or occurrence ids. They must resolve
at build or the build fails; `cadgen step inspect refs` lists the leaves.
A label resolves **into linked children**: a part labelled inside a
sub-assembly you call (`#shoulder_yaw_servo` living in `base_link()`'s
tree) resolves to its occurrence under the link (`o1.1.1`), so an assembly
can mate parts of a sub-assembly it links without owning their geometry.
A ref may name a SUBASSEMBLY as well as a part — a labelled group `Compound`
is an occurrence in the instance tree, and mating it carries every part
beneath it. That is how a rocker-bogie chain is three mates instead of three
hundred; `inspect refs` does not list group refs, because they are not
rendered parts.
- **`axis`** is a selector ref (`axis="#forearm.pivot_bore"` — a cylindrical
face or circular edge yields its axis, a planar face its center+normal) or
literals (`origin=(x, y, z), direction=(x, y, z)`). Refs resolve ONCE at
build into world numbers; the viewer does arithmetic, never topology.
- **ZERO IS THE ARTIFACT AS WRITTEN.** Every DOF's rest value is 0 — the
placement the author built. There is no `default=`; a presentation pose is a
preset. A model that must be WRITTEN at another configuration is authored
at that configuration (or is another model): no decorator argument moves
geometry.
- **`couple(name, {dof: ratio})`** declares a virtual DOF gearing real ones
linearly and ADDITIVELY (setting `curl=x` adds `50*x` degrees to `mcp`).
Exact gear trains are ratio arithmetic, not code.
A geared member BACK-DRIVES in the viewer: when exactly one coupling gears a
DOF with a nonzero ratio, its Pose slider reads the effective value
(own + ratio x coupling), is labelled "driven by <coupling>", and dragging it
moves the COUPLING — `coupling = (target - own)/ratio`, clamped to the
coupling's limits — so sliding one gear turns the whole train. A member's own
value (from a preset or `--kinematics`) is never overwritten, and a DOF geared
by two couplings stays independent: that inverse is underdetermined, so the
viewer refuses it rather than guessing a split.
- A declaration needs at least one mate (or coupling): a pose is a set of joint
values and a joint is what a mate declares, so `poses` alone declare nothing
and are refused. A part with no joints declares no `kinematics=`.
- **`poses`** are named `{dof: value}` presets — all that remains of "pose"
as a concept.
- The mate graph is a TREE: one parent mate per occurrence, no cycles.
Closed-loop linkages (four-bars) are out of scope by design — they need a
solver; the viewer evaluates pure forward kinematics from the sidecar's
numbers at render time.
## Annotating a STEP you did not generate
A document with no model script gets its kinematics from
`cadgen step build IN OUT`, whose `--kinematics` takes the whole SPACE — the
same `{mates, couplings, poses, at}` vocabulary, as inline JSON or a `.json`
path — and whose `--animation` copies a `.js` module's text into OUT's sidecar.
The input is read with OCCT and re-emitted by the canonical writer, so OUT's
bytes are deterministic whichever kernel wrote IN:
```bash
cadgen step build vendor/hinge.step STEP/hinge.step \
--kinematics '{"mates": [{"name": "swing", "kind": "revolute",
"parent": "#body", "child": "#lever",
"axis": "#lever.bore", "limits": [0, 90]}],
"poses": {"open": {"swing": 45}}}'
```
**Wrapper script or `step build`?** A model that will keep changing belongs in a
script — a thin `@step` function that imports the foreign STEP and re-exports
it, so the kinematics live beside the geometry decisions and every edit is one
`python model.py`. Reach for `step build` when the geometry is fixed and not
yours: a one-shot annotation or canonicalization of a vendor file. Re-running it
is a no-op, editing only the kinematics refreshes the sidecar without
re-emitting a byte, and vendor metadata (PMI, GD&T) does not survive the trip.
## Animation: the render module (`<name>.step.js`)
A STEP document may carry ONE JavaScript module beside it, named after the
document: `STEP/arm.step` → `STEP/arm.step.js`. It is the place for
render-only behaviour — today choreography, as the `clips` export below;
other render-only exports will join it, and an export the renderer does not
know is a load ERROR, never ignored. It is an ES module with no imports,
authored by you and COMMITTED even though it lives in a format folder (the
project's `.gitignore` whitelists `*.step.js`; see `project-layout.md`).
```js
// STEP/arm.step.js — beside arm.step; the viewer loads it by name.
export const clips = {
demo: {
label: "Demo",
duration: 8, // seconds
loop: true, // default
update(t, m) { // called every frame; t in seconds
m.get("forearm").rotate([0, 0, 1], 120 * (t / 8), [0, 0, 25]);
m.get("#o1.3.1,o1.3.2").translate([0, 0, 40 * Math.min(t / 2, 1)]);
m.get("lid").opacity(t < 5 ? 1 : 1 - (t - 5) / 2);
},
},
};
```
- `m.get(target)` takes a LABEL (canonical) or occurrence-id refs
(`"#o1.3.1"`, comma lists; each id covers its whole subtree). Unknown
targets THROW — a typo never silently animates nothing. Labels here match
RENDERED PARTS only: to animate a whole group, name its occurrence id.
- Handles: `.rotate(axis, degrees, origin=[0,0,0])`, `.translate(vec)`,
`.opacity(0..1)`, `.visible(bool)`. Successive transform calls
PREMULTIPLY: spin about a part's own center first, then orbit the origin,
and the spin rides the orbit.
- Every frame starts from rest and `update(t)` rebuilds the state — a pure
function of t, so scrub/loop/seek are free. No wall-clock, no state.
- Animation is deliberately Turing-complete and deliberately ignorant of
mates: animating a jointed part re-describes the motion (a few lines of
ratio math). That independence is what guarantees choreography edits can
never invalidate builds.
- No build reads the file. Nothing declares it: drop it beside the document
and the viewer's Animation tab appears on the next load; delete it and the
tab goes. A model without one is simply a model without animation.
- Targets are checked at LOAD, against the compiled tree: every clip's
`update(0, m)` runs once when the module loads, and a label or occurrence
id no part carries is reported in the viewer's Status tab and in
`snapshot --animation`'s error — not at the first frame that reaches it.
- Mesh-only models (no `.step`) have no document to sit beside, and so no
render module; animation is a STEP-document concern.
## Reviewing motion
Snapshot renders stills; motion review is interactive in the viewer. For
still evidence of a configuration, render at DOF values:
```bash
cadgen step snapshot STEP/arm.step tmp/open.png --kinematics '{"jaw": 40}'
```
`--kinematics` is named for the `kinematics=` block it drives, and takes
either spelling: `{dof: value}` JSON, or the NAME of a pose the model
declares under `poses`. A name is checked against the declaration, so a typo
fails with the poses this model actually has:
```bash
cadgen step snapshot STEP/arm.step tmp/open.png --kinematics open
```
For still evidence of a CLIP, freeze one frame: `--animation` names a clip
the document's render module (`STEP/arm.step.js`) declares and `--time` the
moment in seconds (default 0). One frame, one clip, one time — there is no sequence output. The frame
is composed exactly as the viewer composes it: `--kinematics` sets the base
pose, and the clip's `update(t, m)` is evaluated at that time on top of it.
A clip name the model does not declare fails with the clips it has:
```bash
cadgen step snapshot STEP/arm.step tmp/demo_t2.png --animation demo --time 2.0
cadgen step snapshot STEP/arm.step tmp/demo_open.png --kinematics open --animation demo --time 2.0
```
In a JSON job the request is one field, `"animation": {"clip": "demo",
"time": 2.0}`, beside `"kinematics"`; the Python door takes the same object
(`step.snapshot(..., animation={"clip": "demo", "time": 2.0})`) or the clip
name with `time=`.
Identify fixed pivots, link lengths, gear ratios, and joint limits BEFORE
declaring mates; pivot every rotation about its hinge bore or mate face —
never a bounding-box center. Convert visual concerns into `cadgen step
inspect measure` checks before calling them fixed.
references/migrations.md
# Migrations
Read this file when the tooling behaves as though it disagrees with a model you
believe is correct. That is the signature of version skew: a project authored
against an older cadgen, running under a newer one.
cadgen carries no compatibility layer — no shims, no aliases, no deprecated
keyword arguments, no codemod. Every entry point teaches one contract, the
current one, so skew surfaces as ordinary wrongness rather than as a message
about versions. Recognizing it is the reader's job.
## When to suspect skew
- **A model script runs, exits 0, and writes nothing.** An older source carries
no decorated function and no entry point of its own, so Python defines a
function and exits. Nothing looks for an entry point by name.
- **A command or flag you are sure of comes back unknown**, and the help lists
an unfamiliar set. Building a model is running its script; there is no
generation verb, and retired spellings are simply unrecognized arguments.
- **A sidecar is refused for its schema version.** Sidecars are never upgraded
in place and never partially read, because a wrong-shaped one would cost a
model its kinematics silently.
- **A model that used to articulate renders inert**, presenting as a plain
document with no pose and no animation. Nothing is discovered by convention: a
companion `.js` file is read only when a decorator names it.
- **Meshes come out visibly coarser or finer, with no error.** Mesh tolerance
kept its name and changed meaning — chord tolerance is a fraction of the
component's bounding diagonal, not an absolute length — so a value carried
across from an older project is wrong in proportion to the part's own size.
A half-migrated project fails in the wrong place: a correctly converted script
with a stale sidecar beside it fails at the sidecar. Symptoms are only worth
reading once the old artifacts are gone.
## Migration guides
- **cadgen 0.4 → 0.5** — generator functions became decorated model scripts, the
generation CLI was removed, sidecars and provenance moved, snapshot job JSON
was re-keyed, and mesh tolerance became relative.
https://github.com/earthtojake/text-to-cad/blob/main/docs/migrations/migrating-0.4-to-0.5.md
references/positioning.md
# Positioning logic, joints, and mating
Read this file when geometry has mating interfaces, repeated features, assembly children, axes, datums, motion, or user-specified alignment. This is the authoritative reference for assembly positioning, part-local origins, build123d joints, explicit `Location` transforms, CLI `inspect align`, and positioning report content.
## Core rule
Positioning is authored in source and validated after generation. Do not position parts by visually dragging or by editing exported STEP geometry. Use build123d parameters, local coordinate systems, `Location` transforms, `Plane`/`Axis` datums, `cadgen.assembly.AssemblyHelper` relationships, source-level `Joint` objects when useful, and labeled assembly children.
## Terminology
Use these terms carefully:
- **AssemblyHelper** is the preferred generated-script wrapper from `cadgen.assembly`. It records semantic relationships such as `face_to_face`, `coaxial`, `revolute`, and `linear`, then realizes them with native build123d joints.
- **build123d joints** are source-level objects such as `RigidJoint`, `RevoluteJoint`, `LinearJoint`, `CylindricalJoint`, and `BallJoint`. They are attached to `Solid` or `Compound` objects and can reposition parts with `connect_to()`.
- **CLI `inspect align`** is a selector-pair validation tool. It computes a read-only translation delta between selected local refs in a STEP/CAD entry. It does not edit source code, patch exported STEP files, or represent an authored mate feature. This is the one place that distinction is defined; the rest of the skill assumes it.
- **Mating intent** is the design relationship: flush, centered, coaxial, offset, hinge-like, slider-like, or otherwise datum-driven.
Use `AssemblyHelper` and build123d joints to express and compute source assembly placement where appropriate, then use CLI inspection to validate the generated STEP.
## Preferred assembly structure
For assemblies, prefer a mate/joint-driven structure over arbitrary transforms:
```text
root component
→ part-local coordinate systems
→ named datums / joint locations
→ AssemblyHelper semantic relationships backed by native build123d joints
→ labeled Compound assembly with verbose native labels
→ refs/measure/frame/align validation
```
A numeric `Location(...)` should usually correspond to a stated datum, offset, clearance, screw axis, face contact, or joint relationship.
Place a shape with `Pos(...) * shape`, `Rot(...) * shape`, `Location(...) * shape`, or `shape.moved(loc)` — these move the shape and keep its geometry shared. Avoid `shape.located(loc)`: it deep-copies the geometry, which is slower and, for a child model placed in an assembly, breaks the cache's ability to reference the child instead of copying it.
Group a functional unit — a bearing, a gearbox stage, a fastener set — into a sub-assembly node with `asm.add_module(name, children)` when it is placed, reasoned about, or repeated as a unit; nested occurrence refs such as `#o1.12.1` then stay meaningful.
## Part-local positioning
For each part, define a local coordinate convention before modeling:
```text
- Origin: center, base datum, mounting interface, or functional axis.
- XY plane: main sketch/base plane unless another datum is dominant.
- +Z: extrusion/up direction.
- Named dimensions: offsets, hole spacing, boss spacing, clearances.
- Datum features: mating faces, screw axes, centerlines, locating tabs, rails.
```
Good defaults:
- Symmetric standalone parts: origin at body center.
- Plates: origin at footprint center; thickness along Z.
- Enclosures: origin at footprint center; base/lid mating surfaces controlled by Z parameters.
- Shaft/knob/axisymmetric parts: origin on rotational axis.
- Mating adapter plates: origin on the primary mounting datum or center of the bolt pattern.
## Feature placement inside a part
Use named parameters and local coordinates:
```python
hole_offset_x = 30
hole_offset_y = 17.5
hole_positions = [
(-hole_offset_x, -hole_offset_y),
( hole_offset_x, -hole_offset_y),
(-hole_offset_x, hole_offset_y),
( hole_offset_x, hole_offset_y),
]
with Locations(*hole_positions):
Hole(radius=hole_diameter / 2)
```
Avoid untraceable placement constants inside geometry calls. Put all meaningful offsets into parameters.
## AssemblyHelper pattern
Use `AssemblyHelper` for generated assembly scripts. It keeps the LLM-facing code intent-focused while still using native build123d labels, `Joint` objects, and `Compound` assemblies.
```python
from cadgen import build123d as bd, step
from cadgen.assembly import AssemblyHelper
base_height = 30.0
lid_thickness = 3.0
gasket_gap = 0.5
asm = AssemblyHelper("enclosure")
base = asm.add(make_base(), "base")
lid = asm.add(make_lid(), "lid")
base_seat = asm.rigid_frame(
base,
"lid_seat",
bd.Location((0, 0, base_height / 2)),
)
lid_underside = asm.rigid_frame(
lid,
"underside",
bd.Location((0, 0, -lid_thickness / 2)),
)
asm.face_to_face(base_seat, lid_underside, offset=gasket_gap)
@step
def model():
return asm.build()
```
The fixed target is listed first and the moving target second. In the example above, the base stays fixed and the lid moves. The helper is a positioning tool: it calls native build123d `connect_to()` under the hood and its whole output is the placed geometry — nothing about the relationship is recorded or exported. The STEP contains the resolved static placement and native assembly labels, not persistent constraints. Motion that should persist (joints the viewer animates, pose presets) is declared with `kinematics=` on the decorator (`references/kinematics.md`), not with the positioning helper.
Use helper labels intentionally:
```python
standoff = asm.feature(Cylinder(radius=3.0, height=12.0), "m3_standoff", "front_left")
hinge_axis = asm.rigid_frame(lid, "hinge_axis", Location((0, -25, 0)))
```
Assembly labels name the root occurrence. `asm.add()` labels child component occurrences and their exported shape context. For repeated hardware or library parts, use role/location labels such as `front_left` and `rear_right` so STEP topology and viewer selections remain traceable after export.
Feature labels survive best when the labeled geometry remains a child shape in a `Compound`. Labels on boolean-subtracted or fused feature history are not reliable STEP feature history.
Use the frame method that matches native build123d joint inputs: `rigid_frame()` and `ball_frame()` take a `Location`; `revolute_frame()`, `linear_frame()`, and `cylindrical_frame()` take an `Axis` plus optional native range/reference arguments.
## Child dependencies
A child part is wired in one of two modes — a **CHILD** (a model in this
project: import its function and call it; the default) or an **INPUT** (a
document read via `cadgen.read_step`: imported parts, or a generated part the
user explicitly asked to decouple). The modes, what a rebuild tracks, and the
code live in "Composing on other parts" in `step-generation.md`.
Positioning-wise the two are identical: a child is a shape; place it with the
same frames and mates as authored geometry.
**Place a child with `Pos/Rot/Location * child` or `child.moved(loc)` — never
`child.located(loc)`.** `located()` deep-copies the geometry, so the parent
owns a duplicate component instead of linking to the child's tree, and it
also discards any rotation the shape already carried (`build123d-modeling.md`).
`AssemblyHelper` and build123d joints place through `connect_to()` and keep
the link. A mirrored placement is not a placement at all — STEP cannot express
a reflection — so a right-hand part is its own model built from the shared
factory (`step-generation.md`, "Mirrored parts are their own models").
## Imported components
For purchased or downloaded parts (see `$step-parts`), read the STEP file
with `cadgen.read_step` (never `build123d.import_step` — the whys are in
"Composing on other parts" in `step-generation.md`) and add it like any
authored part.
```python
from cadgen import read_step
servo = asm.add(read_step("models/parts/sg90_servo.step"), "servo")
```
Imported geometry was not authored here, so do not assume its origin or orientation. Derive mating frames from inspected geometry: run `refs --facts --planes --positioning` and `measure` against the imported part, then define `asm.rigid_frame(...)` locations from the measured faces, axes, and bolt patterns. Validate the resulting mate exactly like an authored one.
## When to use build123d joints
Use `AssemblyHelper`/build123d joints when assembly intent is clearer as a relationship between part datums than as a raw transform:
- lid-to-base, cover-to-frame, bracket-to-rail, flange-to-pipe, pin-to-hole, shaft-to-bearing
- hinge, slider, screw-like, cylindrical, ball/gimbal, or other motion-positioned assemblies
- repeated or library components that already expose joints
- source assemblies where a change to one dimension should recompute part placement
Direct `Location(...)` transforms are acceptable for simple static layouts when they are parameterized and documented, such as a row of identical spacers or a visual exploded view.
Raw build123d joints are acceptable for advanced cases not covered by `AssemblyHelper`, but preserve the same fixed-first directionality: call `connect_to()` on the fixed/root joint and pass the moving part's joint as `other`. `connect_to()` is a source-generation operation. It repositions the moving part for the generated model; it is not a persistent external constraint in the exported STEP file.
## Joint type selection
Use the simplest joint that expresses the source-level relationship:
- `RigidJoint` / `asm.rigid_frame()`: fixed placement, face-to-face seating, mounting datums, imported components with known interfaces.
- `RevoluteJoint` / `asm.revolute_frame()`: hinge or rotational pose; define with an `Axis` and drive with an angle parameter for a static STEP pose.
- `LinearJoint` / `asm.linear_frame()`: slider, latch, telescoping component; define with an `Axis` and drive with a position parameter.
- `CylindricalJoint` / `asm.cylindrical_frame()`: combined axial translation and rotation, such as screw-like or pin-in-slot relationships.
- `BallJoint` / `asm.ball_frame()`: gimbal or spherical orientation relationship; define with a `Location` and angular ranges.
When only final static placement matters and no meaningful joint datum exists, use explicit `Location` transforms and validate them.
## Assembly positioning workflow
1. Choose the fixed/root component.
2. Define part-local frames and datums before modeling child placement.
3. Identify functional datums such as mating faces, screw axes, hinge axes, sliding axes, locating tabs, gasket offsets, or contact planes.
4. Name source-level joints or mating datums on each child with `asm.rigid_frame()`, `asm.revolute_frame()`, `asm.linear_frame()`, or another helper frame method.
5. Use `AssemblyHelper` relationship methods where they improve source clarity, otherwise use parameterized `Location` transforms.
6. Build a labeled `Compound` assembly with `asm.build()`.
7. Generate the assembly through the Python source, not by re-importing the generated STEP (see `step-generation.md`):
```bash
python path/to/assembly.py
cadgen step inspect refs path/to/assembly.step --facts --planes --positioning
```
## CLI alignment validation
After generation, select moving and target refs from the local selector refs returned by `refs --positioning` and compute deltas:
```bash
cadgen step inspect align path/to/assembly.step \
--moving '#moving_selector' \
--target '#target_selector' \
--mode flush \
--axis z
```
Use `--mode flush` for coplanar face alignment. Use `--mode center` for centerline, plane-center, or symmetrical alignment where supported by the selected references. If the returned delta is outside tolerance, apply a source-level correction (see below), regenerate, and rerun inspection.
## Frame validation
Use `frame` to inspect an occurrence or selector's world frame:
```bash
cadgen step inspect frame path/to/assembly.step '#selector'
```
Use this when:
- a child appears in the wrong orientation
- a mating face is offset in world coordinates
- an axis is expected to align with X/Y/Z
- repeated parts should share orientation
- a downstream task needs a stable coordinate frame
## Measurement validation
Use `measure` for scalar checks:
```bash
cadgen step inspect measure path/to/assembly.step \
--from '#selector_a' \
--to '#selector_b' \
--axis z
```
Examples:
- lid bottom face to base top face should be 0 mm for flush contact
- two screw axes should have matching X/Y positions
- bracket mounting face should sit a specified distance from a datum plane
- spacer height should equal requested offset
## Source-level positioning corrections
When a positioning check fails, fix one of these in source:
- child `Location` translation
- child `Location` rotation
- `AssemblyHelper` relationship fixed/moving order or offset
- build123d joint location or axis
- part-local origin convention
- feature offset parameter
- sketch plane
- workplane selection
- assembly hierarchy
- symmetric placement signs
Then regenerate. Do not patch the exported STEP directly.
## Reporting positioning
In the final response, report only checks that were run:
```text
Positioning/joints:
- source used RigidJoint lid_seat → underside
- base/lid Z mate flush, delta 0.00 mm
- screw boss axis alignment: checked in XY by measurement
- lid occurrence frame: +Z up, origin at assembly centerline
```
If no positioning-sensitive features exist, say:
```text
Positioning: not applicable beyond centered part-local origin.
```
If a mate or alignment was intended but not checked, say `not checked`; do not imply success.
references/project-layout.md
# CAD project structure
Provenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad).
This reference is pure convention: cadgen itself is deliberately unopinionated (a
model script's outputs default to its siblings; `out=` relocates them). Use
this structure for anything bigger than a couple of loose models; skip it for
one-off parts, where a flat folder is fine. Authoring the models themselves is
the `$cad` skill; drawings are `$dxf`.
**Where the project lives**: in a workspace that is more than CAD — a
monorepo, an app with models on the side — put the project inside the
directory that holds the workspace's models (`models/`, for example, or
`cad/`, `hardware/` — whatever the workspace already uses as its home for
CAD; `models/` is only the conventional name), never loose at the root.
In an empty or bare workspace, the CAD project IS the workspace: lay out
`src/` and the format folders at the root.
## The layout: code in `src/`, raw outputs in format folders
Only OUTPUTS are organized by format. Code is not: a model script is not a
"STEP thing" — it is authored Python that happens to emit a STEP.
```
<project>/
src/ # AUTHORED code — the only thing you edit
README.md # the model catalog (see below)
plate.py # one model per file: a part …
plate_drawing.py # … a drawing …
assembly.py # … the root assembly
chassis/ # a SUB-ASSEMBLY folder: its assembly model …
frame.py # … and the parts only it uses (never a module named `chassis`)
strut.py
purchased/ # vendor parts you did not draw: read_step wrapper models
servo.py
lib/ # shared code (plain modules — never models)
__init__.py # one-line docstring; lib is a regular package
holes.py # helpers
bracket_shape.py # a factory two models build from
STEP/ # raw outputs ONLY (+ their sidecars) — and the one authored exception:
plate.step
assembly.step.js # the render module beside a document (choreography); authored, committed
chassis/ purchased/ # outputs mirror src/ folder for folder
imported/ # committed source files brought in from outside (see commit policy)
DXF/ STL/ GLB/ 3MF/ # other format folders: same shape, outputs + imported/
tmp/ # scratch: snapshots, debug renders (gitignored)
```
Two mechanical rules:
1. **Format folders hold only raw artifacts.** Never code, never notes. Each
model script declares its own destination — cadgen has no layout knowledge:
```python
from cadgen import build123d as bd
from cadgen import step
WIDTH = 10.0
@step(out="../STEP/plate.step")
def plate():
return bd.Box(WIDTH, 10, 10)
if __name__ == "__main__":
plate()
```
`out=` resolves relative to the script, so the project relocates as a
unit.
2. **`src/` holds ONLY runnable model scripts.** Every `.py` directly under
`src/` is a model — one parameterless decorated function — that ends with
`if __name__ == "__main__": <model>()`; run it to build it. Everything
shared goes in `src/lib/`: helpers, factories, and constants several models
read. So `ls src/*.py` IS the model catalog. `src/lib/` is a regular
package, not a namespace one: it always contains an `__init__.py`, and a
one-line docstring naming what the package holds is enough.
Because scripts sit directly in `src/`, imports need no setup: a build's
import path is exactly `python script.py`'s — the script's own directory,
`src/`, plus whatever `PYTHONPATH` the project sets — so shared code and
sibling models import directly, from any working directory. A model may share its stem
with the `lib/` module it wraps (`src/body.py` over `lib/body.py`); the two are
different modules (`body` and `lib.body`), so alias the import — `from lib
import body as body_lib` — rather than let the module name shadow the model
function you are about to define. The same shadowing bites a file that needs
**both** a sibling's constants or helpers **and** its model function: `import
base_clamp` binds the module, then `from base_clamp import base_clamp` rebinds
the same name to the function (or the other way round, depending on order).
Alias the module — `import base_clamp as base_clamp_mod` beside `from
base_clamp import base_clamp` — and read constants through the alias. Code in
`lib/` may call a model function too (`from servo import servo` inside a
`lib/` helper pins the child exactly as a call from the parent's own body
does); the child's file must be importable from the caller's `sys.path` (its
folder, or a root the project declares via `PYTHONPATH`), which in this layout
means it sits in the same `src/` (or the same group directory):
```python
from lib import fasteners # a helper module: any edit to it rebuilds this model
from plate import WIDTH # a constant from another model: tracked by value
from plate import plate # another model: a child, tracked by its result
```
Those three imports are the three kinds of dependency a model can have —
**models by result, constants by value, functions by file** — and `cadgen
store why src/<model>.py` shows which ones a model has and whether each is
current. Importing a model never builds it; calling it inside your body does.
Build from anywhere: `python src/plate.py`. Build-if-missing and rebuild are
the same command — the freshness gate runs first, so an unchanged model is a
no-op. There is no project-level build command: regenerate a whole project by
running each script.
```bash
for f in src/*.py; do python "$f"; done
```
The CAD Viewer opened at the project root catalogs the format folders'
artifacts (scripts never appear); before anything is built, discovery is
`src/`, not the viewer.
## Assemblies pull their children
A parent (`assembly.py`) imports its part and sub-assembly models and calls
them in its body; each call builds that child if it is stale — in parallel
with its siblings, on its own worker — or loads it from the store, and the
parent's output LINKS to the child's result. So **running the root is the
whole build**: `python src/assembly.py` rebuilds exactly what is stale beneath
it and nothing else. Dependency is pull, not push: rebuilding a part on its
own (`python src/plate.py`) does NOT rebuild the assemblies that use it —
rerun the parent to pick the change up.
**A sub-assembly is a model** with its own file and its own outputs
(`frame.py` → `STEP/frame.step`), composed into the root exactly like a part.
A sub-assembly with parts of its own is a folder; see "Folders mirror the
product tree". A helper that returns a group of placed parts belongs in a model file, not in
`lib/`: as a model it has a record, builds once, links into every parent and
gives you a STEP to inspect on its own; as a `lib/` function it re-runs inside
every caller and any edit rebuilds them all.
**A mirrored part is its own model.** STEP cannot express a reflection, so a
right-hand part is not a mirrored placement of the left-hand one: put the
shape in a `lib/` factory (`side_bracket(mirrored=False)`) and give each hand
a one-line model file. The template shows the pattern.
**A print-only part is a model too.** `@stl` (or `@glb`/`@threemf`) with no
`@step` declares a model whose outputs are meshes; it composes into
assemblies like any part and writes no STEP.
**The environment is not an input.** Nothing in `src/` or `lib/` reads a
parameter from `os.environ` (or the working directory, the current time, a
random source): the store tracks source by hash, constants by value and children by
result, and cannot see any of those, so a variant selected through them builds
once and then reads as current under every other setting. A configuration is a
factory argument in `lib/`; another configuration is another model file.
## Folders mirror the product tree
`src/` is the import root: every import is spelled from it (`from
chassis.frame import frame`, `from lib import holes`, `from purchased.servo
import servo`), whatever folder the importing script sits in — because the
project declares it with `PYTHONPATH=src` (cadgen never curates paths). One
naming rule makes that hold everywhere: **a folder never contains a module of
its own name.** A build runs like `python script.py`, so the script's own
folder comes first on the import path; from inside `frame/`, the name `frame`
would resolve to `frame/frame.py` instead of the folder, and every spelled
import into that folder fails with "'frame' is not a package". Name the folder
for the subsystem (`chassis/`) and the model for the assembly it builds
(`chassis/frame.py`). Two kinds of folder follow.
**A sub-assembly is a folder.** A folder that holds an assembly model — one
that calls the parts beside it — is a sub-assembly; the parts only it uses live
beside that model, and a part two sub-assemblies share moves up to the level
that owns both. The tree nests as deep as the product does (`base/clamp/`),
and the format folders mirror it one to one (`STEP/base/clamp/clamp.step`), so
the STEP tree reads like the product tree. `purchased/` is the one conventional
folder: `read_step` wrapper models for vendor parts, importable from every
level.
**A group is an independent product.** A project that holds several unrelated
assemblies — a demo corpus, a shop's library — puts each one in its own
directory under `src/`, so one assembly's part models do not pile up beside
another's:
```
<project>/
src/
README.md # catalog: one row per group
rover/ # a GROUP: one assembly and everything it owns
rover.py # the root model (stem = the group's name)
wheel.py suspension.py # its part and sub-assembly models
lib/ # helpers only this group uses
__init__.py
hub.py
gear_stage/
gear_stage.py
STEP/
rover/ # the group's outputs, one folder per group
rover.step wheel.step suspension.step
gear_stage/
gear_stage.step
```
Three rules keep the tree as simple as a flat project:
1. **Groups do not import each other.** A group's root, parts, sub-assembly
folders and drawings live under its one directory. `src/lib/` and
`src/purchased/` are the two folders every group may import; code two
groups both need lives there or is a sign they are one group. Every
import is spelled from `src/` (`from rover.wheel import wheel`, also
inside `rover/`), never relative to the importing file — which is why no
folder may hold a module of its own name.
2. **Outputs mirror the folders.** Every model declares an `out=` that mirrors
its folder path under the format folder (`src/rover/wheel.py` →
`../../STEP/rover/wheel.step`, `src/tom/end_effector/claw_left.py` →
`../../../STEP/tom/end_effector/claw_left.step`; meshes likewise under `STL/`),
so the format folders stay browsable one assembly at a time and the CAD
Viewer opened at the project root shows one folder per assembly.
3. **Every `.py` under `src/` outside `lib/` is a model.** The flat rule holds
at any depth: each script is runnable, and `find src -name '*.py' -not
-path '*/lib/*'` is the catalog. Standalone parts that belong to no
assembly stay directly under `src/`.
The catalog README lists each group with its root; `python
src/<group>/<group>.py` builds that assembly and whatever is stale beneath it.
## Naming
- Model script stem = artifact stem = a Python identifier (`plate.py` →
`STEP/plate.step`). Industry/exchange names (part numbers, revisions,
spaces) go on the ARTIFACT via `out=` ("../STEP/PN-10432_revB.step"),
never into the stem — scripts must stay importable modules.
- A drawing gets its own stem: `plate_drawing.py` → `DXF/plate_drawing.dxf`.
- Every `.py` under `src/` outside `lib/` is a model file; a file may hold
several models (then each writes `<function>.<fmt>` beside it), sharing one closure —
keep them to small variant families.
- A mirrored pair is two stems: `bracket_left.py`, `bracket_right.py`.
- Never distinguish files by case alone (macOS filesystems are usually
case-insensitive).
- Files brought in from outside — vendor downloads, supplier files, anything
used as a SOURCE, whether rendered directly or composed into generated
models downstream — keep their upstream names and live in the format
folder's `imported/` subfolder (`STEP/imported/`, `DXF/imported/`, ...).
Track those folders with Git LFS; `project-template.md` has the
`.gitattributes` to copy.
## Renaming or retiring outputs
Changing a model's `out=` (or deleting a model) does not remove what the old
declaration produced: the previous artifact, its `.step.json` sidecar, and
any declared mesh exports stay on disk and will look like real project files
forever. Treat a rename as an edit PLUS a cleanup, done conservatively:
1. BEFORE editing, list exactly what the old declaration names: the `out=`
target, its `<name>.step.json` sidecar, and each declared export's
`out=` target (`@stl`/`@glb`/`@threemf`).
2. Make the edit, rebuild, and verify the new artifacts exist.
3. Delete ONLY the files from step 1, by exact path. Never glob
(`rm STEP/A*`), never touch `imported/` (those are sources, not outputs),
and when unsure, stop and check `git status` — in a committed project the
orphans appear as exactly the deletions you expect, and anything
unexpected means step 1 was wrong.
## Building many models
Running the root assembly already builds its children in parallel. Distinct
roots fan out safely too: builds never wait on or cancel one another, and two
concurrent runs of one script both complete, with the store keeping the result
whose sources match the files as they are now (nothing corrupts).
```bash
ls src/*.py | xargs -n1 -P4 python
```
Running builds are limited to one per core (`CADGEN_JOBS` overrides); the
rest queue, so a wide fan-out costs no wall time over the ideal. Two costs
worth avoiding: parallel snapshot invocations each pay a headless browser —
batch several views into one `--job` packet instead — and concurrent identical
mesh exports of one document waste work (the shared ledger makes them safe,
not free).
Several agents building one assembly at once: give each its own entry — a
small model that composes only its subsystem — verify there, and build the
full assembly once when the subsystems land; the root then links the
subsystems' results and rebuilds only what changed.
## `src/README.md` — the model catalog
Every project ships a short catalog so an agent landing in the project knows
what builds what without reading every script:
```markdown
# <project> models
| Script | Artifact | Description |
|------------------|-----------------------|--------------------------------------|
| plate.py | STEP/plate.step | Mounting plate, `HOLE_D` corner holes|
| plate_drawing.py | DXF/plate_drawing.dxf | Plate flat pattern |
| frame.py | STEP/frame.step | Plate + two standoffs (sub-assembly) |
| assembly.py | STEP/assembly.step | Frame + left/right brackets (root) |
Build: `python src/assembly.py` builds the root and whatever is stale beneath
it; `python src/<script>` per row for the rest; unchanged models are no-ops.
Imported sources: STEP/imported/servo.step (committed, no script).
```
Keep it a table plus a few lines; update it whenever a model is added or
changed.
## Commit policy (a principle, not a layout)
Derived files are regenerable and typically ignored; authored files and
anything code cannot reproduce are committed:
1. **Authored** (`src/`): always committed.
2. **Generated** (the format folders): NOT committed by default — a fresh
clone regenerates by running the scripts. Snapshots and other review
renders are scratch, not artifacts: they go to `tmp/`, always ignored.
3. **Committed exceptions, made deliberately**: imported source files under
any format folder's `imported/` (no code can regenerate them — a
code-only checkout must never be missing INPUTS, only derived outputs);
render modules (`STEP/<name>.step.js`, the choreography beside a
document — authored, read by no build, so nothing regenerates them); and
pinned fixtures —
anything asserted against byte-for-byte, since regeneration on a newer
kernel can legally change bytes for identical geometry. Pin a loose file
with its own negation line or `git add -f`.
```gitignore
/STEP/*
!/STEP/imported/
!/STEP/*.step.js
/DXF/*
!/DXF/imported/
/STL/*
!/STL/imported/
/GLB/*
!/GLB/imported/
/3MF/*
!/3MF/imported/
/tmp/
__pycache__/
```
Note the `*` forms: ignoring the directory itself (`/STEP/`) would make the
`imported/` negation dead — git never descends into an ignored directory.
## Scaffolding a new project
Copy `project-template.md` (beside this reference) — the full tree with a working part,
drawing, mesh-only part, mirrored pair, two-level assembly, lib modules,
README, and .gitignore to create verbatim. Then verify the loop end to end:
`python src/assembly.py`, snapshot it, and confirm the format folders gained
the artifacts. The template ends with the finished tree — what the project
looks like after that first build, imported source and sidecar included — so
there is nothing else to go and look at.
references/project-template.md
# Project scaffold template
Create these files verbatim (rename `demo`/`plate` to the real project/part),
then run `python src/assembly.py` from the project root to verify the loop.
The finished tree at the end shows the whole project after that first build.
The project root, `demo/` below, is the workspace root when the workspace is
bare, and otherwise sits inside the workspace's existing home for models —
`<workspace>/models/demo/`, or `cad/`, `hardware/`, whatever it already uses;
`models/` is only the conventional name. Everything under `demo/` is the same
either way.
The template is one of everything: a part (`plate`), a drawing of it
(`plate_drawing`), a print-only mesh part (`standoff`), a mirrored pair built
from one factory (`bracket_left`/`bracket_right`), a sub-assembly that places
one child twice (`frame`), and the root assembly (`assembly`).
## `src/plate.py`
```python
"""Demo part: a mounting plate with corner holes."""
from __future__ import annotations
from cadgen import build123d as bd
from cadgen import step
from lib import holes
WIDTH = 60.0
DEPTH = 40.0
THICKNESS = 4.0
HOLE_D = 4.5
@step(out="../STEP/plate.step")
def plate():
body = bd.Box(WIDTH, DEPTH, THICKNESS)
return holes.corner_holes(body, WIDTH, DEPTH, THICKNESS, HOLE_D)
if __name__ == "__main__":
plate()
```
## `src/plate_drawing.py`
```python
"""Demo drawing: the plate's flat pattern (outline + corner holes)."""
from __future__ import annotations
from cadgen import build123d as bd
from cadgen import dxf
from lib import holes
from plate import DEPTH, WIDTH # constants from a model: tracked by value; importing never builds
HOLE_D = 4.5
@dxf(out="../DXF/plate_drawing.dxf")
def plate_drawing():
with bd.BuildSketch() as cut:
bd.Rectangle(WIDTH, DEPTH)
with bd.Locations(*holes.corner_hole_centers(WIDTH, DEPTH)):
bd.Circle(HOLE_D / 2, mode=bd.Mode.SUBTRACT)
return cut.sketch # a bare shape is the CUT layer
if __name__ == "__main__":
plate_drawing()
```
A `@dxf` function returns build123d 2D geometry and the engine writes the DXF —
the same division of labor `@step` has. Return `{layer: shape}` instead when a
drawing genuinely has more than one CAM operation. See the `$dxf` skill.
## `src/standoff.py`
```python
"""Demo print-only part: a standoff that exists as a mesh, never a STEP."""
from __future__ import annotations
from cadgen import build123d as bd
from cadgen import stl
HEIGHT = 12.0
OUTER_D = 8.0
BORE_D = 3.4
@stl(out="../STL/standoff.stl")
def standoff():
return bd.Cylinder(OUTER_D / 2, HEIGHT) - bd.Cylinder(BORE_D / 2, HEIGHT)
if __name__ == "__main__":
standoff()
```
`@stl` alone declares a model whose outputs are meshes: same store record,
same no-op, and it composes into `frame` below like any part. Add `@step` above
it later if a STEP is ever wanted; nothing else changes.
## `src/bracket_left.py`
```python
"""Demo part: the left-hand side bracket."""
from __future__ import annotations
from cadgen import step
from lib.bracket_shape import side_bracket
@step(out="../STEP/bracket_left.step")
def bracket_left():
return side_bracket()
if __name__ == "__main__":
bracket_left()
```
## `src/bracket_right.py`
```python
"""Demo part: the right-hand side bracket — the left one's mirror image."""
from __future__ import annotations
from cadgen import step
from lib.bracket_shape import side_bracket
@step(out="../STEP/bracket_right.step")
def bracket_right():
return side_bracket(mirrored=True)
if __name__ == "__main__":
bracket_right()
```
STEP cannot express a reflection, so a mirrored part is its own model: the
shape lives once, in the factory, and each hand is a one-line model with its
own STEP, its own record and its own place in the assembly.
## `src/frame.py`
```python
"""Demo sub-assembly: the plate carrying two standoffs."""
from __future__ import annotations
from cadgen import build123d as bd
from cadgen import step
from plate import THICKNESS, WIDTH, plate # the model (by result) and two constants (by value)
from standoff import standoff
PITCH = WIDTH / 2
@step(out="../STEP/frame.step")
def frame():
base = plate() # built if stale, else loaded; LINKED
base.label = "plate"
post = standoff() # a mesh-only child links like any other
left = bd.Pos(-PITCH / 2, 0.0, THICKNESS / 2) * post # placed: one link …
left.label = "standoff_left"
right = bd.Pos(PITCH / 2, 0.0, THICKNESS / 2) * post # … placed again: a second link, one tree
right.label = "standoff_right"
return bd.Compound(children=[base, left, right], label="frame")
if __name__ == "__main__":
frame()
```
Place children with `Pos/Rot/Location * child` or `child.moved(loc)` — never
`child.located(loc)`, which copies the geometry and turns the link into a
duplicate component.
## `src/assembly.py`
```python
"""Demo root assembly: the frame between its two brackets."""
from __future__ import annotations
from cadgen import build123d as bd
from cadgen import step
from bracket_left import bracket_left
from bracket_right import bracket_right
from frame import frame
from plate import DEPTH, THICKNESS
SPAN = DEPTH / 2 + 6.0
@step(out="../STEP/assembly.step")
def assembly():
core = frame() # a sub-assembly: its tree, linked
core.label = "frame"
left = bd.Pos(0.0, -SPAN, THICKNESS) * bracket_left()
left.label = "bracket_left"
right = bd.Pos(0.0, SPAN, THICKNESS) * bracket_right()
right.label = "bracket_right"
return bd.Compound(children=[core, left, right], label="assembly")
if __name__ == "__main__":
assembly()
```
Running `python src/assembly.py` builds every stale model beneath it — the
brackets, the frame, and through the frame the plate and the standoff — in
parallel, and links their results. Rebuilding a part alone does not rebuild
this root; rerun it to pick up the change.
## `src/lib/__init__.py`
```python
"""Shared helpers for the demo project: hole patterns and the bracket factory."""
```
`src/lib/` is a regular package, so this file is never omitted — one line naming
what the package holds is the whole file.
## `src/lib/holes.py`
```python
"""Shared hole helpers (plain module: no @step here)."""
from __future__ import annotations
from cadgen import build123d as bd
INSET = 6.0
def corner_hole_centers(width: float, depth: float):
"""The four corner-hole centers, shared by the part and its drawing."""
return [
(sx * (width / 2 - INSET), sy * (depth / 2 - INSET))
for sx in (-1, 1)
for sy in (-1, 1)
]
def corner_holes(body, width: float, depth: float, thickness: float, hole_d: float):
for x, y in corner_hole_centers(width, depth):
body -= bd.Pos(x, y, 0) * bd.Cylinder(hole_d / 2, thickness * 2)
return body
```
## `src/lib/bracket_shape.py`
```python
"""The side-bracket factory: one shape, two hands (plain module: no @step here)."""
from __future__ import annotations
from cadgen import build123d as bd
LENGTH = 40.0
HEIGHT = 10.0
THICKNESS = 6.0
HOLE_D = 5.0
def side_bracket(mirrored: bool = False) -> bd.Shape:
body = bd.Box(LENGTH, THICKNESS, HEIGHT)
body -= bd.Pos(LENGTH / 4, 0.0, 0.0) * bd.Rot(90, 0, 0) * bd.Cylinder(HOLE_D / 2, THICKNESS * 2)
return bd.mirror(body, about=bd.Plane.YZ) if mirrored else body
```
A helper in `lib/` is part of the SOURCE of every model that imports it: any
edit here rebuilds both brackets (and, on their next run, the assemblies that
use them). That is the right behaviour for a factory — and the reason shared
code that is really a sub-assembly should be a model file instead.
## `src/README.md`
```markdown
# demo models
| Script | Artifact | Description |
|------------------|------------------------|---------------------------------------|
| plate.py | STEP/plate.step | Mounting plate, `HOLE_D` corner holes |
| plate_drawing.py | DXF/plate_drawing.dxf | Plate flat pattern |
| standoff.py | STL/standoff.stl | Print-only standoff (mesh only) |
| bracket_left.py | STEP/bracket_left.step | Left side bracket |
| bracket_right.py | STEP/bracket_right.step| Right side bracket (mirror image) |
| frame.py | STEP/frame.step | Plate + two standoffs (sub-assembly) |
| assembly.py | STEP/assembly.step | Frame + both brackets (root) |
Build: `python src/assembly.py` builds the root and whatever is stale beneath
it; `python src/plate_drawing.py` for the drawing; unchanged models are no-ops.
Imported sources: STEP/imported/servo.step (committed, no script).
```
`STEP/imported/servo.step` stands for any source file brought in from outside
(a vendor download, a supplier's model) under its upstream name: no script
produces it, so it is committed, and a model script that composes it reads it
with `cadgen.read_step` anchored on the script's own location — scripts build
from any working directory, so the path is
`Path(__file__).parent / "../STEP/imported/servo.step"`, never a bare relative
string (the `$cad` skill covers `read_step`, and how to wrap an import in a
model of its own so assemblies can link to it).
## `.gitignore`
```gitignore
/STEP/*
!/STEP/imported/
!/STEP/*.step.js
/DXF/*
!/DXF/imported/
/STL/*
!/STL/imported/
/GLB/*
!/GLB/imported/
/3MF/*
!/3MF/imported/
/tmp/
__pycache__/
```
The `*` forms matter: ignoring the directory itself (`/STEP/`) would make the
`imported/` negation dead — git never descends into an ignored directory. The
`!/STEP/*.step.js` line keeps render modules (the authored choreography beside
a document, `arm.step.js` beside `arm.step`) committed while everything
generated around them stays ignored. A project whose folders mirror the
product tree (groups, sub-assembly folders, `purchased/`) needs the negations
at every depth, because git never descends into an ignored directory:
```gitignore
/STEP/*
!/STEP/imported/
!/STEP/*.step.js
!/STEP/*/
/STEP/*/*
!/STEP/*/*.step.js
!/STEP/*/*/
/STEP/*/*/*
!/STEP/*/*/*.step.js
```
One `!/STEP/*/`, `/STEP/*/*`, `!/STEP/*/*.step.js` triple per nesting level
(the same for `DXF/`, `STL/`, `GLB/`, `3MF/`). Pin any other file deliberately
with its own negation line or `git add -f`.
## `.gitattributes`
```gitattributes
# Imported sources are binaries you did not write: keep them out of plain git history.
STEP/imported/** filter=lfs diff=lfs merge=lfs -text
DXF/imported/** filter=lfs diff=lfs merge=lfs -text
STL/imported/** filter=lfs diff=lfs merge=lfs -text
GLB/imported/** filter=lfs diff=lfs merge=lfs -text
3MF/imported/** filter=lfs diff=lfs merge=lfs -text
# Authored text beside the documents stays plain git.
*.step.js text
*.step.json text
```
A per-project file at the project root, next to `.gitignore`. The `imported/`
folders hold vendor STEPs, supplier DXFs and other files you did not author;
tracking them with Git LFS keeps those binaries out of plain git history while
the render modules and sidecars beside the documents stay diffable text. Run
`git lfs install` once per machine. If a file under `imported/` is a few lines
of text beginning `version https://git-lfs...`, it is an LFS pointer whose
object was not fetched: `read_step` fails on it, and
`git lfs checkout STEP/imported` (or the folder in question) is the fix.
## Verify
```bash
python src/assembly.py # builds the root and, beneath it, frame, plate, standoff, both brackets
python src/assembly.py # "current" — the no-op gate works
python src/plate_drawing.py # builds DXF/plate_drawing.dxf (the drawing is not under the root)
cadgen store why src/frame.py # the frame's record: its two children, pinned and current
cadgen step snapshot STEP/assembly.step tmp/assembly.png
```
## The finished tree
After the verify loop, the project is complete and looks like this — the one
exemplar this structure needs:
```
demo/
.gitignore
.gitattributes
src/ # committed — the only thing anyone edits
README.md
plate.py
plate_drawing.py
standoff.py
bracket_left.py
bracket_right.py
frame.py
assembly.py
lib/
__init__.py
holes.py
bracket_shape.py
STEP/
plate.step # generated by src/plate.py — ignored
bracket_left.step
bracket_right.step
frame.step
assembly.step
assembly.step.js # the render module beside assembly.step — authored, committed
imported/
servo.step # brought in from outside — committed
STL/
standoff.stl # generated by src/standoff.py — ignored
DXF/
plate_drawing.dxf # generated by src/plate_drawing.py — ignored
tmp/
assembly.png # the snapshot: scratch — ignored
```
No model here declares kinematics or a mesh export beside its STEP, so no
`.step.json` sidecar is written; a model that does gets one beside its
document, generated with it and ignored with it. `assembly.step.js` is the
other file beside a document: the render module (choreography — see the cad
skill's kinematics reference). Nothing generates it and no build reads it; the
viewer loads it by name, so it is authored like `src/` and committed like
`imported/`.
`git status` in this tree shows exactly `.gitignore`, `.gitattributes`, `src/`,
`STEP/assembly.step.js` and `STEP/imported/servo.step`: authored code, the
authored choreography, and the one input code cannot regenerate. Everything
else is rebuilt by running the scripts, so a fresh clone that runs
`python src/assembly.py && python src/plate_drawing.py` arrives at this same
tree.
references/repair-loop.md
# Repair loop
Read this file when generation, export, inspection, positioning, snapshot review, CAD Viewer setup, or documentation validation fails.
## Loop
1. Read the failing command output.
2. Classify the failure.
3. Make the smallest responsible source or command change.
4. Rerun the failed command.
5. Rerun any dependent validation checks.
6. Report remaining risk or deliberate deviations.
## Failure classes and fixes
### Multi-section loft: "Failed to create valid loft" / "Recovery failed"
The message names neither the station nor the cause. Two checks, in this order:
1. **Loft increasing PREFIXES** (`faces[:5]`, `[:10]`, `[:20]`, …) to bracket
where it breaks, and watch the reported volume as well as the exception — a
loft that "succeeds" with an absurd volume is already failing.
2. **Loft every ADJACENT PAIR.** If every pair succeeds but the full set fails,
the sections are individually fine and the problem is global — almost always
that sections disagree on POINT COUNT. Guarantee a fixed sample count per
section.
Two silent causes worth ruling out before either:
- **A section that is genuinely disconnected** (two closed regions — e.g. a
station cutting two separate nacelles, or crossing an open slot) produces a
`Face` that raises nothing and reports a plausible area; only `Face.is_valid`
is False. The loft then fails dozens of stations away. End the loft at the last
connected station, or bridge the gap in the section and cut it back afterwards.
(`Face.is_valid` is a PROPERTY — calling `f.is_valid()` raises
`TypeError: 'bool' object is not callable`, which reads like a corrupt object.)
- **Samples dropped where a component does not exist** make counts vary station
to station. Carry a value rather than dropping the sample.
### Boolean against a large lofted surface never returns
A subtract against a single large B-spline surface costs a full-surface
classification PER TOOL and grows superlinearly in tool count — measured on one
~4,900-control-point skin: 1 tool 24 s, 4 tools 70 s, 41 tools did not finish in
15 minutes, and a 44-tool build ran over seven hours without completing. Batching
into one list operand does NOT help; the cost is per tool, not per accumulation.
Confirm rather than guess: the process stays at ~100 % CPU with the progress file
frozen on its first phase, and a stack sample shows `Extrema_ExtPS::Perform` with
`BSplSLib_Cache::BuildCache` rebuilding on nearly every evaluation.
Fix by not cutting: shallow cosmetic recesses do not need to be booleans at all.
At 19 m rendered to 1920 px, 1 px is ~10 mm, so a 4 mm groove is sub-pixel and
reads only because the edge overlay draws feature edges. Keep booleans for
openings that change the silhouette, and build the rest additively.
### Source import or syntax failure
Likely causes:
- invalid Python syntax
- missing import
- wrong build123d symbol
- function not named the `@step` model function
- executable code outside the intended function has side effects
Fix:
- correct imports and syntax
- ensure the `@step` model function returns the STEP-ready shape or compound
- keep output paths in CLI commands, not inside the `@step` model function
### Invalid or missing geometry
Likely causes:
- open sketch
- subtractive profile outside target
- zero thickness
- boolean operation failed
- construction geometry used as exported geometry
Fix:
- close profiles intended to become faces
- verify dimensions are positive
- make subtractive tools pass through when through-cuts are intended
- simplify the failing feature and rebuild incrementally
### Fillet or chamfer failure
Likely causes:
- radius/length exceeds local geometry
- selected edges include tiny or unintended edges
- boolean operation created complex edge topology
Fix:
- reduce radius/length
- filter selected edges more narrowly
- apply fillets later in the model
- split edge groups by feature intent
### Wrong scale or bounding box
Likely causes:
- units mismatch
- mistaken diameter/radius
- extrusion direction or amount wrong
- part not centered as assumed
- direct imported STEP uses unexpected units
Fix:
- check parameter values
- inspect facts and planes
- measure critical extents
- correct source dimensions or import handling
### Missing feature
Likely causes:
- wrong `Mode.ADD`/`Mode.SUBTRACT`
- feature profile not inside target
- blind cut too shallow
- selector changed after prior operation
Fix:
- confirm feature mode
- increase cut length for through-cuts
- inspect topology or planes
- regenerate and measure/check feature-specific refs
### Selector fragility
Likely causes:
- arbitrary index selection
- topology changed after fillet or boolean
- similar faces/edges are indistinguishable
Fix:
- select by axis, plane, position, normal, or inspected reference
- use `refs --facts --planes --positioning` to rediscover stable references
- add construction datums or simplify operations if needed
### Positioning or joint mismatch
Likely causes: wrong part-local origin or datum, reversed `AssemblyHelper` fixed/moving order, `.connect_to()` moving the wrong part, inverted joint axis, sign errors in symmetric placement, an explicit `Location` not recomputed after a parameter change, or a joint defined in world coordinates when a part-local datum was intended.
Fix:
- inspect `refs --positioning`, then `frame` and `align` on the relevant selectors
- verify the source-level `AssemblyHelper` target order, joint labels, and `joint_location` definitions
- apply the smallest source correction from the list in `positioning.md` (Source-level positioning corrections)
- regenerate the assembly from the Python source and rerun the failed check
### CAD Viewer startup or link failure
Likely causes:
- Node/npm unavailable
- CAD Viewer app not built or cannot start
- Viewer URL path is not the project's absolute model directory
- returned link is missing `?file=`, or its `file=` is not relative to that directory
Fix:
- rebuild each link as `<viewer-origin><absolute-model-directory>?file=<path relative to it>`
- return one documented Viewer link per requested file
- if unresolved, report the startup failure and rely on CLI facts/measurements plus snapshots for validation
### CAD `cadgen step snapshot` failure
Likely causes:
- target input path is wrong, missing, or not a STEP/STP file or same-stem Python generator
- adjacent CAD Viewer GLB/topology artifact missing
- invalid render flags
Fix:
- generate STEP first, then snapshot the primary `.step`/`.stp` artifact
- retry only with simpler supported snapshot jobs, starting with a single `view` output before wireframe display or `section`
- choose modes and packet size per `snapshot-review.md`
## Diff after repair
Use `diff` when the fix might have affected unrelated geometry:
```bash
cadgen step inspect diff path/to/before.step path/to/after.step --planes
```
## Reporting failed repairs
If a check cannot be repaired in the current environment, report:
```text
- what failed
- what was tried
- which artifact is still usable
- which validation claims cannot be made
- what the next source-level correction should be
```
references/snapshot-review.md
# Snapshot review
Read this file when choosing saved CAD `cadgen step snapshot` outputs for primary STEP/STP artifacts.
## Policy
Snapshot validation is mandatory. Every created or visibly updated primary STEP/STP part or assembly gets at least one reviewed PNG snapshot; deterministic checks passing is not a reason to skip. Use CAD `cadgen step snapshot` rather than opening the viewer manually or using Playwright; snapshots are faster, lighter, more precise, and more agent-friendly. Snapshots are PNG stills; review motion interactively in the viewer. For still evidence of a pose or of one moment in a clip, pass `--kinematics` and/or `--animation CLIP --time SECONDS` (see `kinematics.md`, "Reviewing motion") — one frame, never a sequence.
Skip saved snapshots only when no visible geometry was created or updated, or no valid artifact exists:
- pure format/export requests where geometry is unchanged
- source changes that do not alter visible geometry
- inspection-only tasks (for example direct measurement questions) that create or update nothing
- failed Python or STEP generation before a valid artifact exists
When skipping, report the reason and the deterministic evidence that still ran.
Do not loop on snapshots. Rerender only when a source repair changed visible geometry or when a specific visual finding needs confirmation.
## Packet sizing
One PNG is enough for a simple static part. Use the small multi-view packet when semantic errors are plausible from shape complexity or prompt intent:
- assemblies or more than one body/part
- holes on multiple faces or multiple axes
- shells, internal cavities, bores, passages, open enclosures, or section-critical features
- ribs, gussets, bosses, standoffs, slots, cutouts, lightening holes, fins, blades, or repeated patterns
- source repairs after a geometry, boolean, selector, or feature failure
- prompts where "looks like the requested object" is part of the task
- deterministic checks pass but visible semantics are still uncertain
## Small packet
Prefer a single `view` JSON job with these outputs:
```json
{
"input": "models/part.step",
"mode": "view",
"outputs": [
{ "path": "/tmp/render/iso.png", "camera": "iso" },
{ "path": "/tmp/render/iso_opposite.png", "camera": { "direction": [-1, 1, -0.8] } },
{ "path": "/tmp/render/top_ortho.png", "camera": "top" },
{ "path": "/tmp/render/front_ortho.png", "camera": "front" }
],
"render": { "viewLabels": true, "padding": 0.12, "sizeProfile": "diagnostic" }
}
```
The two opposed isometric views guarantee every face appears in at least one image — rear, left, and bottom features are covered by default, not by suspicion. The top ortho is the primary pattern/symmetry check and the front ortho the profile check.
Set `input` to the primary STEP/STP artifact using a relative or absolute path (documents only — a `.py` model script is refused: run it first, then snapshot the STEP it wrote). The snapshot CLI derives its internal render root from that input path. It defaults to `theme: "snapshot"` and `display.mode: "solid"`. `snapshot` is a render-only theme — Workbench Light with the ground grid, origin axis and shadows removed, because in a still image those read as geometry rather than as orientation. It is not offered in the CAD Viewer's theme picker; pass `theme: "workbench-light"` to match the viewport exactly; labeled/section views default to 1600x1200 when dimensions are omitted. Use `render.sizeProfile: "assembly"` or `"assembly-large"` for complex assemblies that need 1800x1200 or 1920x1440. For CAD review packets, use still-image render modes `view` and `section`; set `display.mode` to `solid`, `transparent`, `hidden_edges`, `hidden_lines_removed`, or `wireframe` when the visual check benefits from explicit CAD linework.
Use `--focus '#o1.2' ...` to emphasize specific part or subassembly occurrence refs — in `view` renders the focused refs keep full opacity while the rest of the assembly is ghosted in place (framing and context are preserved); in `section` mode focus isolates the refs entirely. Use `--hide '#o1.2' ...` to omit parts from the render in every mode. Do not combine focus and hide in the same snapshot command or job. These filters accept occurrence refs only, not face, edge, vertex, or shape selectors.
## Output paths
Name the file and you get that file:
```bash
cadgen step snapshot STEP/bracket.step tmp/review.png
# then Read tmp/review.png
```
OUT (and an output's `path` in a JSON packet) is written exactly as given, with a relative path resolved against the current working directory. The target is deleted before the render starts and the finished image is written atomically, so the file at that path is always the render you just ran.
1. **Tight iteration: reuse one name.** Render, Read, edit the source, render again to the same `tmp/review.png`. Every read is provably the latest render, because a failed one leaves nothing to read.
2. **Comparisons: name the iterations.** Use `tmp/before.png` and `tmp/after.png` when both images are genuinely needed.
3. **A missing file IS the failure signal.** A nonzero exit or a file-not-found means the render failed; there is never an older image at the path to mistake for output.
Pass a directory (`tmp/` as OUT, or an output `path` that is one) only when the name does not matter: a timestamped name is generated inside it, and that is the one case where you read the path from the `saved snapshot:` line.
## Targeted additions
Add views only when the brief or a failure mode calls for them:
- reference-image reproduction: one snapshot from the reference image's viewpoint for side-by-side comparison
- `section`: shell, bore, internal cavity, passage, blind hole, enclosure, or wall/floor relationship
- `display.mode: "solid"`: shaded CAD view with explicit edge linework
- `display.mode: "rendered"`: shaded material view without edge overlay
- `display.mode: "transparent"`: overlap, collision, enclosure readability, or hidden contact checks when transparency adds information and wireframe is too noisy
- `display.mode: "hidden_edges"`: opaque shaded context with hidden/occluded CAD edges visible through solids
- `display.mode: "hidden_lines_removed"`: line-focused review where hidden/occluded edges should be suppressed
- `display.mode: "wireframe"`: internal overlap, hidden interference, or assembly collision suspicion when full triangle wire is useful
- labeled or annotated review: use supported CAD Viewer refs, selections, screenshots, or GUI review links
Exploded or labeled review is an intent, not a render mode. Satisfy it through supported CAD Viewer mechanisms, supported JSON job settings, or the GUI link.
## Diagnostic review
Visual review is diagnostic, not authoritative. Convert every visual concern into a follow-up geometry check before using it as a validation claim:
- hole pattern appears asymmetric -> measure hole centers and compare offsets
- lid, child part, or occurrence appears offset -> inspect frames and mating deltas
- gusset, boss, standoff, rib, or plate may be floating -> inspect solid count, labels, connectivity, contact, or relevant distances
- cavity, bore, or blind hole looks wrong -> run section review, then measure wall thickness, depth, or through-condition
- repeated pattern looks uneven -> measure pattern centers, angular spacing, or occurrence frames
Final reports should include the generated snapshot PNGs or the documented skip reason, and state which deterministic checks support any visual finding.
references/step-generation.md
# The model contract and STEP generation
Read this file when authoring or rebuilding a model script, composing models
into assemblies, deciding what a rebuild tracks, or working with imported
STEP/STP files.
## The model script is the tool
Generation has no CLI. A model is a plain Python script whose `__main__` calls
the decorated function; that call builds it:
```python
from cadgen import build123d as bd
from cadgen import step
WIDTH = 10.0
@step
def bracket():
return bd.Box(WIDTH, 10, 10)
if __name__ == "__main__":
bracket()
```
```bash
python bracket.py # builds bracket.step (and the result tree in the store)
python bracket.py --force --json # per-run flags ride the script's argv
```
Every run keeps the model's result in the store current — a **tree** of exact
`.brep` + `.surf` components plus links to its children's trees — and writes
every output the model declares from that result. Unchanged sources are a
fast no-op. The default `.step` is the sibling `<stem>.step`; relocate it
durably with `@step(out="path/to/out.step")` (relative to the script). There
is no per-run output override: a model has one set of outputs, declared in its
decorators, and the store's record of it is keyed by the script.
Rules the decorator enforces:
- **The decorator only declares.** Nothing runs at decoration or import time.
A model file without `if __name__ == "__main__": <model>()` never builds.
- **A top-level call builds.** Calling the decorated name when no build is in
progress (`__main__`, a REPL) runs the pipeline and returns `None`; a failed
build exits with the pipeline's code. It takes no arguments.
- **A call inside a build composes.** From another model's body the same name
returns the shape: the child is built if it is stale (writing ITS outputs
and record), otherwise loaded from the store, and either way its result is
linked into the parent's. Composition is ordinary Python; there is nothing
to cache by hand and no composition API.
- **One model per file is the recommendation, not a rule.** A model's identity
is `script.py::function`; a file holding one model is named by its path alone
(`python plate.py`, `store why plate.py`). Several decorated functions in one
file are allowed — a small variant family — and each is its own record,
output (a sole model writes `<file>.<fmt>`; models sharing a file write
`<function>.<fmt>`) and job, built by its
own call under `__main__`; name one as `plate.py::plate_wide` in `store why`
and `store forget`. They share the file's closure: editing any of them makes
all of them stale, so a family that changes independently belongs in
separate files.
- **Calling a model from plain Python returns its geometry.** Outside a build,
`plate()` builds (or finds current) and returns the model's tree as a
`Compound` — what a parent composing it would get — so a script, a notebook
or a REPL can read bounds, faces or volumes straight off a model. A drawing
returns `None`.
- **A model takes no parameters.** It is one configuration of one set of
outputs, so there is nothing for an argument to select; the decorator
refuses a parameter list. Parametric geometry is a plain factory the model
calls:
```python
from cadgen import build123d as bd
from cadgen import step
def _bracket(width: float, thickness: float) -> bd.Shape:
return bd.Box(width, 10, thickness)
@step
def bracket():
return _bracket(width=40.0, thickness=6.0)
if __name__ == "__main__":
bracket()
```
A second configuration is a second model (`bracket_wide.py`), with its own
outputs — the way two part numbers are two parts. Values a model shares with
its drawing or its assembly live in module constants (`WIDTH = 40.0`) that
the siblings import.
- **The return is a bare build123d `Shape` and nothing else** — a dict return
is refused. The return IS the geometry: a `Compound` placing children is
packaged as occurrences (linked where a child is another model's result), a
single solid as one component. Nothing is declared or inferred about it —
`inspect` and the run both report `part`/`assembly` off the tree.
- **Outputs are what the decorators declare.** `@step` writes the `.step`.
Mesh outputs are `@stl`/`@threemf`/`@glb` stacked on the model, tolerances
on the decorators (`supported-exports.md`). **A model may declare no STEP at
all**: `@stl`/`@threemf`/`@glb` with no `@step` is a full model — same tree,
record, build, no-op and composition — that writes its meshes and no
`.step`, no sidecar. STEP is one output kind, not a requirement.
- Options on `@step`: `out=`, `mesh_tolerance=`, `mesh_angular_tolerance=`,
`kinematics=` (`kinematics.md`). **No decorator argument changes the
geometry a model produces**: they decide where the files land, how they are
written, and what the sidecar declares. No decorator names JavaScript:
choreography is the render module beside the document
(`STEP/<name>.step.js`), which the viewer loads by name and no build reads.
Everything a model declares about itself lives in its decorators, and a
child's declarations never ride up into a parent.
**Imports:** `from cadgen import build123d as bd` is the canonical import — a
lazy, transparent re-export (same names, same objects on first touch), so a
current model's re-run never pays the ~2.5s kernel import: the freshness gate
and the warm-worker handoff fire before any `bd.` attribute resolves. Raw
`import build123d` still works, just slower on re-runs (the build prints a
one-line hint). Keep `bd.<anything>` out of module-level constants and default
arguments for the same reason.
**A model runs like `python script.py`.** Its folder is on `sys.path` for the
whole build, plus your `PYTHONPATH` — cadgen adds nothing else and infers no
project root — so an import inside the body, or inside a helper the body calls,
resolves exactly like one at module top — and the file it loads is hashed when
it executes, so it is in the closure either way. Prefer module-top imports for
readability and so the static scan sees the graph up front; a lazy import is
not an error.
## Generated vs imported STEP
These two terms classify a STEP file by what its source is:
- A **generated STEP file** has a model script as its source. The STEP is a
*derived output*; the script is what you edit and re-run.
- An **imported STEP file** is its own source: authored or downloaded
elsewhere. There is nothing upstream to regenerate.
A model that DECLARES something beyond geometry — kinematics, animation, or
mesh exports — gets a sidecar BESIDE THE OUTPUT (`<name>.step.json`) carrying
those sections. A plain model writes NO sidecar: its record in the store is
what makes reruns no-op. Imports write none of it. The written STEP file
itself carries NO cadgen metadata and no link back to source code, ever — a
bare artifact copied anywhere is a plain importable file, and every door
resolves it by its bytes, so a moved or copied document renders identically to
its twin.
## Composing on other parts: children and inputs
A model that builds on another part wires it in one of two modes. Choose
deliberately:
- **A CHILD (the default)** — the other part is a model in this project:
import its function and call it. A child edit flows into the parent on the
parent's next rebuild; there are no exported bytes to keep in sync. Never
route a generated child through its exported `.step`.
- **An INPUT** — the other part is a document, not source: a purchased or
downloaded part, or a generated part the user has EXPLICITLY asked to
decouple (export it once, then treat the export like any other document).
Read it with `cadgen.read_step`, below.
### Children
A child is just an import: model scripts are real modules, and
`from widget import widget` binds the model with no build side effects.
Calling `widget()` inside the parent's body builds the child when it is stale
(writing the child's own outputs) or loads its result from the store, and
returns the shape. What comes back is GEOMETRY only — tree, labels, colors,
placements. A child's sidecar content (its mates, kinematics, animation) never
rides up into the parent: declare what the assembly needs on the assembly.
```python
from cadgen import build123d as bd
from cadgen import step
from link_pin import link_pin # importing binds; never builds
@step(out="../STEP/link_arm.step")
def link_arm():
bar = bd.Box(40.0, 8.0, 4.0)
bar.label = "bar"
pin = link_pin() # built if stale, else loaded
left = pin.moved(bd.Location((-15.0, 0.0, 2.0))) # placed: the parent LINKS to the pin
left.label = "pin_left"
right = pin.moved(bd.Location((15.0, 0.0, 2.0))) # placed again: a second link, one tree
right.label = "pin_right"
return bd.Compound(children=[bar, left, right], label="link_arm")
if __name__ == "__main__":
link_arm()
```
**Link or component.** Place a child's shape as it came back — `moved()`,
`Pos/Rot/Location * child`, relabelled, recolored — and the parent's result
LINKS to the child's tree (stored once, shared by every parent; two placements
are two links to one tree). Modify it (a boolean, a mirror, extracting a
sub-shape) and the parent owns that geometry as its own components; the
dependency is tracked either way. **Never `located()`** for placement: it
deep-copies the geometry, which makes it the parent's own component instead
of a link (`positioning.md`). Put geometry changes that belong to the child in
the child's file or its factory.
**Every build is parallel.** A child call returns at once with a lazy shape
and submits the child's build to the pool; the body keeps calling siblings,
each landing on its own worker; the parent waits when it first reads geometry
— normally the closing `bd.Compound(children=[...])`, after every sibling has
been submitted. Placement (`moved`, `Pos/Rot/Location *`), `.label` and
`.color` are deferred; anything that reads geometry (`.faces()`,
`.bounding_box()`, a boolean, `copy.copy`) forces that child there, so
parallelism follows the dependencies the body actually expresses. Nothing is
annotated and nothing is scheduled ahead of time.
**Dependency is pull.** A parent depends on each child by RESULT: its record
pins the child's tree hash, so a child edit that yields identical geometry
leaves the parent current, and an edit that does not reach a child skips that
child's Python and kernel work entirely. **Rebuilding a child does not rebuild
the assemblies that use it** — run the parent to pick up the change
(`python src/robot.py` builds whatever is stale beneath it and links the
rest). A parent finished against a child that changed during its build says so
(`already stale: … rerun`).
**Builds never wait on or cancel one another.** Two runs of one model both
run to completion; each publishes what it built and the store keeps the one
whose sources match the files as they are now. Editing a child while its
parent builds leaves the parent finished against the child it pinned — its
next gate says stale (`store why` shows the pinned vs current tree). There is
no lock anywhere.
### What a rebuild tracks — models by result, constants by value, functions by file
What an importer TAKES from a model file decides how that file counts:
- **`from widget import widget`** (the model function) → tracked by RESULT:
the parent pins the child's tree; `widget.py` is not in the parent's source.
- **`from widget import WIDTH`** (a module-level literal: a number, string,
bool, `None`, or tuples/lists/dicts of those) → tracked by VALUE: a
comment or body edit in `widget.py` leaves the importer current; only a
changed value rebuilds it.
- **Anything else** from a model file (a helper function, a `bd.` object, an
expression) → tracked by FILE: the whole file joins the importer's source
closure, and any edit to it rebuilds the importer. Shared helpers therefore
belong in `lib/` (a plain module, in the closure of every model that
reaches it), and shared constants may live in a model file or in `lib/`.
Inputs join the closure too: a `read_step` document is hashed as a build
input. The render module beside the document (`<name>.step.js`) is NOT one —
it is the viewer's, and editing it never makes a model stale.
Every decorator argument is ordinary Python, evaluated when the module is
imported: `out=f"{FOLDER}/{NAME}.step"`, `mesh_tolerance=TOL` with `TOL` from
`lib/`, a path built from a constant — all fine, and nothing is read off the
source text. The values feeding them are tracked like any other input (a
`lib/` module by file, a model-file constant by value), so changing the
constant behind an `out=` makes the model stale. The module top must still stay
kernel-free: what a door pays to learn a model's declarations is one import of
the file.
### Models inside a package
A model file may live inside a Python package (folders with `__init__.py`).
cadgen runs it under its dotted name, so relative imports (`from .parts.washer
import washer`) resolve whenever cadgen loads the model: as a child of another
model, or when you run it as a module (`python -m pkg.stack`). Running the file
by path (`python pkg/stack.py`) is Python's own limit, not cadgen's: Python
executes it as `__main__` with no package, so a relative import fails before
cadgen is involved; use `-m` or absolute imports for a file you run directly.
`PYTHONPATH` still declares any import root beyond the script's own folder;
cadgen adds nothing of its own.
### Mirrored parts are their own models
STEP cannot express a reflection, so a right-hand part is not a mirrored
placement of the left-hand one: give it its own model file that calls the same
factory, and let the assembly place two ordinary children.
```python
# src/lib/bracket_shape.py — the factory (plain module, no decorator)
from cadgen import build123d as bd
def side_bracket(mirrored: bool = False) -> bd.Shape:
body = bd.Box(40.0, 10.0, 6.0) - bd.Pos(12.0, 0.0, 0.0) * bd.Cylinder(2.5, 6.0)
return bd.mirror(body, about=bd.Plane.YZ) if mirrored else body
```
```python
# src/bracket_left.py
from cadgen import step
from lib.bracket_shape import side_bracket
@step(out="../STEP/bracket_left.step")
def bracket_left():
return side_bracket()
if __name__ == "__main__":
bracket_left()
```
```python
# src/bracket_right.py
from cadgen import step
from lib.bracket_shape import side_bracket
@step(out="../STEP/bracket_right.step")
def bracket_right():
return side_bracket(mirrored=True)
if __name__ == "__main__":
bracket_right()
```
Mirroring a child inside the parent (`bd.mirror(bracket_left(), ...)`) is
legal — the parent then owns the mirrored geometry as its own components — but
the right-hand part has no STEP of its own and no place to declare exports or
mates. Prefer the model.
### Inputs: reading a STEP file the model does not generate
Use `cadgen.read_step`, not `build123d.import_step`. It returns the same
shape, served from the op memo on a warm run, and — the part that matters — it
RECORDS the file's content hash as a build input. Replacing the vendor STEP
then makes the model stale on its own, with no `--force`; read through
build123d and the model stays "current" against a file that changed
underneath it.
```python
from pathlib import Path
from cadgen import read_step, step
_HERE = Path(__file__).resolve().parent
@step
def rig():
motor = read_step(_HERE / "imported" / "vendor_motor.step") # recorded input
...
```
An imported part is an INPUT, not a model: nothing links to it and it has no
record. To make it first-class — so assemblies link to it, so it has its own
outputs and declarations — wrap it in a model of its own:
```python
from pathlib import Path
from cadgen import read_step, step
_HERE = Path(__file__).resolve().parent
@step(out="../STEP/servo.step")
def servo():
return read_step(_HERE / ".." / "STEP" / "imported" / "sg90_servo.step")
if __name__ == "__main__":
servo()
```
**Never `read_step` your own output.** A model that reads the `.step` it is
about to write is not a loop — it is a model whose input changes every time it
runs, so the gate can never say "current", every build is a full rebuild, and
the geometry depends on what the last run happened to leave on disk. Keep
source documents where the model cannot write them — placement policy belongs
to `project-layout.md` (`imported/`). Input path and output path being different
files is the whole rule. If the geometry you want is something the project
already builds, call that model instead of reading the artifact.
For structuring multi-part projects (folder layout, shared `src/lib/` code,
commit policy), read `project-layout.md` and `project-template.md`.
## Freshness: `cadgen store why`
`cadgen store why <model>.py` (or a generated `.step`; the store remembers
which script wrote it) is the one freshness door. It prints the gate's verdict
clause by clause and why:
```text
model /abs/src/frame.py
verdict STALE (child result moved: standoff.py)
[ok] 1 record present
[ok] 2 closure 1 files unchanged
[x] 3 children (2)
[ok] /abs/src/plate.py pinned 51b0eafbbc5f current 51b0eafbbc5f
[x] /abs/src/standoff.py pinned 33df4f5cf2ee current 91667e73758b child result moved
[ok] 4 tree 7dc0cee81f77 complete
[ok] 5 outputs (1)
[ok] /abs/STEP/frame.step
closure b99f36c995b8 files: frame.py
tree components 0 occurrences 0 links 3
link plate -> 51b0eafbbc5f
link standoff_left -> 33df4f5cf2ee
link standoff_right -> 33df4f5cf2ee
```
Here `standoff.py` was edited and rebuilt on its own; the frame still pins
the old tree, so it is stale until `python src/frame.py` runs — the pull
semantics above, made visible. The exit code is 1 for stale, 0 for current;
`--json` gives the same verdict as data. The five clauses: (1) a record
exists; (2) the closure files — and any constant imported by value — hash as
recorded; (3) every child is current and its tree is the one pinned; (4) the
tree and its components exist in the store; (5) every declared output matches
its recorded sha. Mesh tolerances and argv flags are not inputs. Reach for
`store why` whenever a model did or did not rebuild when you expected it to,
before reaching for `--force`.
## Generated assemblies
An assembly is a model whose return places children (a `Compound` of parts
or of other models' results); the tree records that structure and `inspect`
reports it as `assembly`. Passing a generated assembly's exported `.step` to a tool treats it as a document and
loses source-level composition; work with the `.py` source. Prefer
`cadgen.assembly.AssemblyHelper` so native labels, named mate frames, and
source-level relationships are preserved before STEP export (see
`positioning.md`).
## Imported STEP/STP files
An imported STEP/STP file needs no model script and no preparation step. Hand
it straight to `cadgen step inspect`, `cadgen step snapshot`, or a mesh door:
each compiles a tree from the file's bytes on first use (a job in the pool,
shared with the CAD Viewer), and its part/assembly kind is inferred from the
STEP product hierarchy.
```bash
cadgen step inspect refs path/to/imported.step --facts
cadgen stl build path/to/imported.step meshes/imported.stl
```
To produce STL/3MF/native GLB files from an imported STEP, pass it to the
matching format door with an explicit OUT (an imported file declares nothing, so
a bare door has no variants to produce); read `supported-exports.md`.
### Re-emitting a foreign STEP as your own
A STEP written by another kernel round-trips through cadgen with
`cadgen step build IN OUT`: OCCT reads it, the tree is built, and the
canonical writer emits it, so OUT's bytes are deterministic and identical on
every run. The same command ANNOTATES a document that has no model script —
`--kinematics` takes the whole space (`{mates, couplings, poses, at}`, the same
vocabulary the decorator takes, as inline JSON or a `.json` path) and
`--animation` copies a `.js` module's text into OUT's sidecar.
```bash
cadgen step build vendor/hinge.step STEP/hinge.step \
--kinematics '{"mates": [{"name": "swing", "kind": "revolute",
"parent": "#body", "child": "#lever",
"axis": "#lever.bore", "limits": [0, 90]}],
"poses": {"open": {"swing": 45}}}'
```
Re-running is a no-op; editing only the kinematics refreshes the sidecar without
re-emitting a byte. Vendor metadata (PMI, GD&T) does not survive the round trip.
**Choose the door by how the model will evolve**: a shape you will keep changing
belongs in a model script (a thin wrapper that reads the foreign STEP), while
a one-shot canonicalization or annotation of a file you do not own is exactly
what `step build` is for.
## Optional-module assemblies
A model that imports several part modules and SKIPS the ones that do not exist
yet is a useful pattern for parallel work — the assembly stays renderable while
individual parts are still being written. It has one sharp edge.
The model's closure is computed from the modules it ACTUALLY IMPORTED at build
time. A module that did not exist during the build was never in the closure,
so its later appearance cannot make the model stale, and every door keeps
reading the old document's tree — no error, no warning. Run the model script
explicitly after adding a part module rather than relying on the gate.
## After generation
- Confirm the process succeeded and each declared output exists and is
non-empty (the stdout line names the document; `--json` adds the `tree`
hash).
- Run the baseline inspection and any spec-driven checks per
`inspection-and-validation.md`:
```bash
cadgen step inspect refs path/to/model.step --facts --planes --positioning
```
## Workers and the daemon
Every build is a job on a worker; every worker has build123d imported. A warm
daemon runs them **by default** — the decorator hands a directly-run script to
it before any kernel import — and `CADGEN_DAEMON=0` uses transient workers
instead:
```bash
python path/to/part.py # warm: persistent workers
CADGEN_DAEMON=0 python part.py # transient workers, spawned for this run
```
- **One worker per model.** A request lands on the worker bound to its model
script; a busy worker means a second one (an *extra*) runs the job now; a
model with no worker takes a warm spare (`CADGEN_DAEMON_SPARES`, default 2,
refilled in the background). Nothing waits on another build and no worker
count is capped.
- **Children build in parallel.** Inside a body, each child call submits that
child to the pool and returns at once; siblings build on their own workers
while the body continues, and the parent waits only when it first reads the
geometry.
- **One running build per core.** `N = os.cpu_count()` jobs run at once
(`CADGEN_JOBS` overrides); the rest queue in order. A parent waiting on its
children holds no slot, so a deep tree builds on a single slot. Hitting the
limit during a fan-out is normal and costs no wall time.
- **Idle workers unbind after 10 minutes** (`CADGEN_DAEMON_IDLE_UNBIND`,
seconds) and return to the spare set; the daemon exits after an hour with no
request (`CADGEN_DAEMON_IDLE_TIMEOUT`). Both are about RAM; neither ever
blocks a build.
- **No memory ceiling and no worker cap.** Unlimited memory is the operating
assumption. A worker the OS kills mid-job is reported as a dead worker with
its exit status, the job it held, and the exact `CADGEN_DAEMON=0 ...` rerun;
nothing is retried silently.
- **`CADGEN_DAEMON=0` is still parallel.** Transient workers are spawned for
the run (each paying one kernel import, concurrently), inherit the
environment — so a test's `CADGEN_CACHE_DIR` isolates its store — and exit
with the run. There is no daemon job ledger in this mode, so the CAD Viewer
does not see such builds in progress.
- **`cadgen daemon status`** reports each worker's model, whether it is busy,
its job count and whether it is an extra; the spare count; and `jobs running
n/N, queued m` — the first place to look when a build seems slow.
- Doors (`inspect`, `snapshot`, the mesh doors) never run a body and take no
slot; a compile of a document with no tree is the one door operation that
is a job.
- The daemon runs on Windows too (a named pipe instead of a Unix socket). It
is per cadgen install; `CADGEN_DAEMON_SOCKET` overrides the address, and a
`.log` beside it holds lifecycle and C-level OCP noise. When cadgen itself
changes, the daemon notices the version token mismatch, drains its jobs and
exits; the next client starts a fresh one.
Cold and warm builds write identical bytes for every format.
references/supported-exports.md
# Supported exports
Read this file when the user requests STL, 3MF, or native GLB output files from CAD geometry. For a `.step` file, run the model script (see `step-generation.md`) — a mesh door writes mesh formats only. For 2D DXF output, use the `$dxf` skill: a drawing is its own `<name>.py` declaring one `@dxf` function — one model per file, so a drawing never shares a script with a `@step` model.
## Policy
STL, 3MF, and native GLB are mesh exports, not substitutes for STEP. Validate the primary CAD geometry first, then export the requested formats. Do not treat exported mesh renders as CAD validation; inspect and snapshot the primary model per the standard workflow.
Native GLB exports are ordinary glTF 2.0 binary files for external tools: Y-up, with one material per distinct part/face color. Do not confuse them with what the CAD Viewer renders from — the model's result tree in the store (`~/.cache/cadgen`: content-addressed exact-geometry components plus links to child trees), which every build writes and a mesh door never does.
## Declare the exports the model always has
A mesh output that belongs to the model belongs in the model. Stack `@stl`, `@glb` or `@threemf` on the `@step` function and every build produces them:
```python
from cadgen import build123d as bd
from cadgen import glb, step, stl
@step(out="STEP/bracket.step")
@stl(out="STL/bracket.stl")
@glb
def bracket():
return bd.Box(40, 20, 6)
if __name__ == "__main__":
bracket()
```
`python models/bracket.py` then writes the STEP **and** the declared meshes, and rewrites any of them that were deleted or edited — no separate export step (a declared output is part of the model's freshness gate). The declarations are recorded in the document's sidecar, which is where the mesh doors read them from.
## A model with no STEP
A model's outputs are whatever its decorators declare, and STEP is one output kind, not the primary. A function decorated with `@stl`, `@glb` or `@threemf` alone — no `@step` — is a full model: the same tree and record in the store, the same build, the same parallel children, the same no-op when nothing changed, the same composition (`spacer()` inside another model's body links its tree like any child). It writes its declared meshes and no `.step` (and no sidecar). Use it for a print-only part or a render asset; there is no requirement to write a STEP. Review it with its format's snapshot door (`cadgen stl snapshot STL/spacer.stl tmp/spacer.png`); `cadgen store why spacer.py` explains its freshness exactly as for a STEP model.
```python
from cadgen import build123d as bd
from cadgen import stl
@stl(out="STL/spacer.stl", mesh_tolerance=4e-4)
def spacer():
return bd.Cylinder(6, 3) - bd.Cylinder(2.5, 3)
if __name__ == "__main__":
spacer()
```
Stacking order stays neutral: add `@step` above or below later and the same declarations ride along; the `.step` then joins the outputs.
A decorator `out=` is the one intentional exception to native path semantics: on `@stl`, `@glb` and `@threemf` — exactly as on `@step` — a relative `out=` resolves relative to the SCRIPT, not the working directory. That is what makes a project relocatable: the declaration travels with the model and produces the same layout whatever directory the script is run from. Ad-hoc OUT arguments on the doors are cwd-relative instead, because they are one-shot and never persisted.
Declare the same format more than once at distinct targets for draft/print variants:
```python
@stl(out="STL/bracket_draft.stl", mesh_tolerance=8e-3)
@stl(out="STL/bracket_print.stl", mesh_tolerance=4e-4)
```
## Tool
One door per format — `cadgen stl build`, `cadgen 3mf build`, `cadgen glb build` — each taking a STEP/STP **document** and an optional output path:
```bash
cadgen stl build STEP/model.step # every declared @stl variant
cadgen stl build STEP/model.step meshes/model.stl # one ad-hoc export
```
Doors take documents, never scripts: `python model.py` is the one source door, and a door handed a `.py` says so. Omitting the output is the normal form: it produces exactly what the model declared, read from the document's sidecar. A document that declares no variants of that format has nothing to produce — declare `@stl` on the model and rerun the script, or name an explicit OUT. An explicit OUT takes the same native path semantics as every other door: a relative path resolves against the current working directory, an absolute path is used as given, and `~` expands. Ask for several formats by running several doors — each writes only its own format:
```bash
cadgen stl build STEP/model.step
cadgen 3mf build STEP/model.step
cadgen glb build STEP/model.step
```
An output the model already has at the requested tolerances is reported `current` and not rewritten. `--force` re-exports it anyway; it never rebuilds the model itself — rerun `python <script>` for that. The door reads the document's tree by the file's content hash and compiles one from the bytes if the store has none; whether the document is behind its script is not the door's question (`cadgen store why`), so no document is ever refused.
An imported STEP/STP file declares nothing, so give it an explicit OUT; its part/assembly kind is inferred automatically:
```bash
cadgen stl build path/to/imported.step meshes/imported.stl
```
A mesh door never writes a `.step` file. A generated model's STEP is the OUTPUT of `python <model>.py`; an imported model's STEP is already the file on disk.
## Rendering a mesh file
Each mesh format also has a `snapshot` verb, with the same `TARGET [OUT]` grammar `cadgen step snapshot` uses:
```bash
cadgen stl snapshot STL/bracket.stl tmp/bracket_mesh.png
cadgen 3mf snapshot 3MF/bracket.3mf tmp/bracket_3mf.png
cadgen glb snapshot meshes/bracket.glb tmp/bracket_glb.png
```
A mesh carries no CAD topology, so these render shaded solid and do not HAVE `--focus`/`--hide`, `--display`, `--kinematics`, `--animation`/`--time`, or `--mode section` — a mesh has no occurrences, CAD edges, kinematics, or clips for those to act on, so they are absent from the command rather than refused by it. `cadgen step snapshot` refuses a mesh input and names the door that takes it.
This is a review of the EXPORT, not of the model. Snapshot validation of the primary STEP is still what the required workflow means; render the mesh when the question is about the mesh (tessellation density, a tolerance change, what an external tool will receive).
## Mesh tolerance
Mesh exports tessellate each component's exact surfaces with the same watertight tessellator the CAD Viewer renders with, at the same default tolerances — an export matches what renders, boundary vertices lie on the exact STEP edge curves, and repeated exports are byte-identical.
Use these flags when the default mesh density is wrong for the part:
```bash
--mesh-tolerance FLOAT # chord tolerance RELATIVE to each component's
# bounding diagonal (default 1.5e-3)
--mesh-angular-tolerance FLOAT # max normal spread across a triangle edge,
# radians (default 0.35)
```
Either flag overrides what the declaration and the model set, for that run only. Use tighter tolerances for visual fidelity on curved parts; use looser tolerances for large simple geometry when file size matters. The linear tolerance is relative (scale-free), not an absolute deflection in millimetres.
## Workflow
1. Validate the model per the standard workflow (build, inspect, snapshot).
2. Declare the exports the model should always have; run the model script.
3. For anything ad hoc, run the format door for each requested format.
4. Report the exported files.
Example — the model declares its STL, and a one-off coarse GLB is requested beside it:
```bash
python models/bracket.py
cadgen glb build models/bracket.step meshes/bracket_preview.glb \
--mesh-tolerance 5e-3 \
--mesh-angular-tolerance 0.5
cadgen step inspect refs models/bracket.step --facts --planes --positioning
```
## Reporting
```text
Files:
- STEP: /absolute/project/models/bracket.step
- STL: /absolute/project/models/STL/bracket.stl
- GLB: /absolute/project/models/meshes/bracket_preview.glb
Validation:
- CAD geometry validated; STL/3MF/native GLB written as requested exports.
- Primary STEP/STP snapshot packet run/skipped and why.
```
requirements.txt
cadgen[snapshot]==0.5.0
SKILL.md
---
name: cad
description: Create, modify, inspect, and validate parametric CAD parts and assemblies authored as cadgen model scripts. Use for natural-language CAD specs, reference images, 2D technical drawings, STEP/STP generation or direct inspection, Python CAD source, source-level joints, selector references, geometry facts, measurements, mating deltas, snapshots, and STL/3MF/native GLB outputs from CAD geometry. Also covers project structure for multi-part CAD work - src/ for model scripts and shared code, format folders (STEP/, DXF/, STL/) for raw outputs, naming, and commit policy for projects with several @step/@dxf model scripts and imported source files; use it when starting a CAD project with more than a couple of models, when asked how to organize CAD code and artifacts, or when growing a flat folder of models into a project.
---
# CAD generation, inspection, and validation
Provenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad).
Use the installed local skill files as the runtime source of truth; the
repository link is only for provenance and release review.
## Setup
This skill's commands are thin entrypoints over the `cadgen` distribution, which
carries the Python build runtime and the JavaScript it executes. Install it once:
```bash
python -m pip install -r requirements.txt
```
Rendering additionally needs a browser, which pip cannot supply:
```bash
python -m playwright install chromium
```
## Purpose
Create or modify parametric CAD models from natural-language requirements, build validated STEP/STP (or mesh) outputs, inspect geometry references, and return checked outputs. STEP is the default output of CAD geometry and the one the inspection tools read; STL, 3MF, and native GLB are mesh outputs a model declares beside it — or instead of it, when the part is print-only. For assemblies, prefer `cadgen.assembly.AssemblyHelper` with source-level build123d joints, named mating datums, and native labels when the parts have functional assembly relationships.
There are two ways into the STEP workflow: build from a build123d model script (the default when designing from scratch or modifying a generated model), or import an existing STEP/STP file directly (when no script exists or the user explicitly targets the STEP file). Both are inspected, snapshotted and exported the same way.
## Use this skill when
Use this skill when the user asks for CAD files, STEP/STP files, build123d source, selector refs such as `#o1.2.f1`, mechanical parts, assemblies, enclosures, brackets, fixtures, holes, counterbores, countersinks, slots, pockets, bosses, standoffs, ribs, fillets, chamfers, shells, source-level joints, mating, or measurements. Also use it when the user supplies reference images or 2D technical drawings of a part to reproduce or take design intent from.
Also use it when the user asks for STL, 3MF, or native GLB output from CAD geometry; load `supported-exports.md` for details. For 2D DXF drawings, use the `$dxf` skill; when a DXF projects from a 3D part, this skill owns the part and `$dxf` owns the drawing.
Do not use this skill for render-only concept art, CAM toolpaths, engineering certification, FEA conclusions, architectural BIM, or freehand illustration unless the user also needs CAD geometry.
## Default assumptions
Use these defaults unless the user specifies otherwise. These are first-pass modeling defaults, not manufacturability, tolerance, or certification claims:
- Units: millimeters.
- Origin: per the part-type defaults in `references/positioning.md`; center of the main part or assembly when nothing better applies.
- Base plane: XY.
- Up/extrusion axis: positive Z.
- Output geometry: closed, positive-volume solids unless the user requests surfaces or construction geometry.
- STEP structure: one valid solid, a compound of solids, or a labeled assembly compound.
- Assembly structure: fixed root part, part-local frames, named mating datums, `AssemblyHelper` relationships backed by build123d joints where applicable, explicit generated placements, and verbose native labels.
- Small plastic enclosure wall: 2.0-3.0 mm when unspecified.
- Cosmetic fillet: 1.0-3.0 mm when safe for local geometry.
- M3/M4/M5 normal clearance holes: 3.4/4.5/5.5 mm unless another standard is requested.
Ask one focused clarification question only when missing information makes the model impossible, fit-critical, safety-critical, or compliance-bound. Otherwise proceed with explicit assumptions.
## Tools and paths
The command surface (the `cadgen` console script, installed with the package):
```bash
python <model>.py # its __main__ calls the model, which builds it
cadgen step build IN OUT # re-emit an existing STEP as a new one, with kinematics
cadgen stl build ... # one door per mesh format; `3mf` and `glb` are the others
cadgen step inspect ... # refs, measure, align, frame, diff
cadgen step snapshot ... # PNG visual review packets, for STEP
cadgen stl snapshot ... # the same, for a mesh file; `3mf` and `glb` again
cadgen store why <model>.py # why the model is stale or current, clause by clause
cadgen daemon status # the warm workers and the jobs they are running
```
**Scripts are RUN; commands take DOCUMENTS.** `python model.py` is the one
source door — it writes every output the model declares and (only when the
model declares kinematics, animation, or mesh exports) its sidecar. Every
command above takes a `.step`/`.stl`/`.dxf` FILE, and one handed a `.py` says
so. A door asks one question of a document: does the store have a tree for
this file's bytes? If so it reads it; if not it compiles one from the bytes as
a job in the pool — generated or imported alike. **A door never refuses a
document and never runs a script.** Whether a document is behind its script
is the model's business (`cadgen store why`), not the door's.
Use the active project Python interpreter; treat `python` in examples as an interpreter placeholder. Every operational verb is a `cadgen` subcommand (`python -m cadgen.cli <verb>` is the PATH-independent equivalent). Use `cadgen <verb> --help` for the complete current interface; reference docs show recommended workflows, not every flag. Install per `requirements.txt`; `cadgen doctor <skill-dir>` verifies the installed cadgen matches this skill's pin (docs drift silently on a mismatched install).
Target paths resolve from the command's current working directory, not from the skill directory. Run commands from the workspace that owns the artifacts and pass cwd-relative target paths so project CAD files never resolve accidentally under the skill directory.
CAD references are `#...` selector tokens local to a target, for example `#o1.2` or `#o1.2.f1`. Pass the STEP/CAD file as a separate target argument when using CAD CLIs.
## A model
Generation has NO CLI. A model is a plain Python script: one parameterless
decorated function, built by calling it from `__main__`:
```python
from cadgen import build123d as bd
from cadgen import step
WIDTH = 10.0
@step # or @step(out="../STEP/bracket.step") to relocate the output
def bracket():
return bd.Box(WIDTH, 10, 10)
if __name__ == "__main__":
bracket()
```
The rules, each enforced by the decorator or the build:
- **The decorator only declares; a call builds.** Importing a model module
never builds; a file without `if __name__ == "__main__": <model>()` never
builds either — always end the script that way. `python bracket.py` writes
`bracket.step` beside the script and the model's result into the store; an
unchanged model is a fast no-op. `--force` rebuilds this model only.
- **A model takes no parameters** and its function is called with no
arguments. Parametric geometry lives in a plain factory the model calls
with its values (`def _bracket(width, thickness): ...`); another
configuration is another model in another file, the way two part numbers
are two parts.
- **The return is a bare build123d `Shape`** — a solid, a compound, or a
labeled assembly compound. Never a dict, never a path.
- **Outputs are exactly what the decorators declare.** `@step` writes the
`.step`; `@stl`/`@threemf`/`@glb` stacked on it write meshes. **STEP is not
required**: a function with only `@stl` (or `@glb`, `@threemf`) — no `@step`
— is a full model with the same tree, record, build and no-op, whose outputs
are the meshes and which writes no `.step` and no sidecar. Use it for
print-only parts and render assets. `references/supported-exports.md`.
- **Decorator arguments never change the geometry.** They decide where the
files land (`out=`), how they are written (`mesh_tolerance=`,
`mesh_angular_tolerance=`) and what the sidecar declares (`kinematics=`).
The geometry is the return value and nothing else: a `Compound` placing
children is packaged as occurrences, a single solid as one component, and
`part`/`assembly` is read off the tree. There is no `kind=` and no bake
point — a posed or differently configured export is authored geometry, or
another model.
- **A sidecar only when strictly necessary.** `<name>.step.json` is written
only when the model declares `kinematics=`; a model that declares none has
no sidecar, and a rebuild that dropped the declaration deletes the stale
file. What a model declares about its outputs lives in its record, not in
a file beside the geometry.
- **One model per file, as a rule of thumb.** A model's identity is its file
plus its function (`plate.py::plate`); a file holding one model is named by
its path alone. A file MAY hold several (a small family of variants): each is
its own record, output and job (a sole model writes `<file>.step`; models sharing a
file write `<function>.step`), but
they share the file's closure, so editing one rebuilds them all — which is
why one per file is the recommendation.
- **Composition is a call.** Import a sibling model and call it inside your
body (`from arm import arm` … `arm()`); it returns the child's geometry.
`references/step-generation.md` has the whole composition contract.
- **`from cadgen import build123d as bd`** is the canonical import — a lazy,
transparent re-export of build123d (same names, same behaviour) — so the
freshness gate and the warm-worker handoff run before any kernel import is
paid. Raw `import build123d` works but costs ~2.5s on every re-run.
- Per-run flags ride the script's argv: `--force`, `--json`, `--verbose`,
`--mesh-tolerance`, `--mesh-angular-tolerance`.
## Composition, freshness and builds
The essentials; `references/step-generation.md` has the code and the edge cases.
- **Children are models you call.** A parent's body imports sibling models
and calls them; each call returns that child's geometry (built if stale,
loaded from the store if current), and the parent's result LINKS to the
child's — stored once, shared by every parent. Place a child with
`Pos/Rot/Location * child` or `child.moved(loc)`; never `child.located(loc)`
(it deep-copies the geometry, so the parent owns a copy instead of linking).
- **Every build is parallel.** A child call submits the child's build and
returns at once; siblings build on their own workers while the body keeps
going; the parent waits when it first reads the geometry — normally the
closing `bd.Compound(children=[...])`. Nothing to configure, nothing to
annotate.
- **Builds never wait on or cancel each other.** Two runs of one model both
run; the store keeps the result whose sources match the files as they are
now, so the disk ends at the newer source. Editing a child while its parent
builds leaves the parent finished against the child it pinned.
- **A rebuilt part does not update the assemblies that use it.** Dependency
is pull: rebuild the parent (`python assembly.py`) to pick up a child's
change. A child edit that yields identical geometry leaves parents current.
- **What a rebuild tracks — models by result, constants by value, functions by
file.** Importing a model function tracks that model by its result;
importing a module-level literal (`from plate import WIDTH`) tracks the
value; importing anything else from a file (a helper function, a `bd.`
object) makes that whole file part of your model's source, so any edit to it
rebuilds you. Shared constants may live in a model file or in `lib/`.
- **The environment is not an input.** Model and `lib/` code takes no
parameter from `os.environ`, the working directory, the current time or a
random source: the gate tracks source by hash, constants by value and children by
result, and cannot see any of those — a value that changes geometry through
them leaves a stale result reading as current. A configuration is a factory
argument; another configuration is another model.
- **A mirrored part is its own model.** STEP cannot express a reflection, so
a right-hand part is a separate model file calling the same factory with
`mirror=True` (or mirroring the factory's result), not a mirrored child.
- **`read_step` files are inputs, not models.** Replacing the file makes the
reader stale. To make an imported part first-class, wrap it:
`@step def servo(): return read_step(...)`.
- **`cadgen store why <model>.py`** is the freshness door: it prints the
gate's verdict clause by clause (record, closure files, constants, each
child's pinned vs current tree, tree objects, declared outputs). Reach for
it whenever a model did or did not rebuild when you expected it to.
**Workers.** A warm daemon is on by default: each model gets a persistent
worker (a second, an *extra*, when the model is asked for while already
building); spares stand by so a new model never pays the import; idle workers
unbind after ten minutes. Running builds are limited to one per core
(`CADGEN_JOBS` overrides); a parent waiting on its children holds no slot.
`CADGEN_DAEMON=0` uses transient workers spawned for that one run — still
parallel, still the same store — and is the mode for tests and debugging.
`cadgen daemon status` lists workers, spares and the running/queued jobs.
**Debugging notes.** Do not alternate `CADGEN_DAEMON=0` and daemon runs of one
model while a daemon build of it is in flight (the two are unbrokered; each
publishes what it built, and the publish rule keeps the newer source). **One
project, one store.** A build under another `CADGEN_CACHE_DIR` (a temp store,
a test) rewrites the same output files; the first store's records then see
outputs whose bytes they did not write, so its gate reports the model stale
(`output changed: …`) and every parent `child stale: …` — nothing is wrong,
the two stores simply disagree, and the next build under either settles it.
**Module bodies stay cheap.** A model file is imported on every rerun, before
the gate: a module-level `read_step` (computing a layout from a vendor STEP at
import) pays the kernel and the parse each time even when the model is
current — call `read_step` inside the body or a function it calls; the
`hint:` printed on such a run names the import site. **Resets, smallest
first:** `python model.py --force` rebuilds one model now; `cadgen store
forget <model.py>` drops its record so the *next* run rebuilds it (children
untouched); `cadgen store forget <file.step>` drops the tree entry for that
file's bytes so the next open or door call compiles it again; `cadgen store
gc` sweeps unreachable objects; **clearing the store (`rm -rf
~/.cache/cadgen`, or `$CADGEN_CACHE_DIR`) is always safe** — every model
reads as stale and rebuilds, and no project file is touched. The gate has no
cadgen-version clause, so a model built by a cadgen with a bug stays current
after the fix: `forget` the affected models (or the parents that link them),
or clear the store.
**The store** (`~/.cache/cadgen`, `CADGEN_CACHE_DIR` overrides) holds
`objects/` — immutable, content-addressed components and trees — and `index/`
— the per-model records the gate reads, the op memo, and the mesh ledger. It
contains only derived results. The full contract is `STORE.md` in the
installed `cadgen` package.
## Streams, progress and failures
**Streams.** stdout carries the result; stderr carries progress, timing, and failures. A model run prints `<outcome> <document path>` on stdout (`built`, `current`, or `skipped-peer` when a concurrent build of the same model finished first), and the two streams never interleave, so `2>/dev/null` leaves a clean parseable result and `>/dev/null` leaves a readable log. JSON on stdout is always compact; pipe through `jq .` to read it. For machine-readable output: model runs, the `build` doors (`step`, `stl`, `3mf`, `glb`) and `snapshot` take `--json`; `inspect` already emits JSON and takes `--format text` for prose. A model run's `--json` line carries `outcome`, `document` and `tree` (the result's hash). `--verbose` adds stage timing (and full tracebacks) on stderr. Output volume does not grow with model size.
**The build tree.** On a terminal, stderr shows the graph as the body's child calls reveal it — one refreshed block, each model `submitted`, `queued`, `building · <phase> n/total`, `current`, or `✓ <time>`, finished subtrees folded to one line. With `--json` or a non-TTY, one JSON line per model transition (`model`, `parent`, `state`, `phase`, `progress`, `elapsed`) on stderr replaces the drawing; the result line on stdout comes last. After publishing, the root re-runs its gate once and says `already stale: <child> changed during the build; rerun` if it did.
**Reporting progress from a model.** A long build spends most of its wall time inside
the model body. Import the reporter — it binds to whichever build is running, and does
nothing when there is none:
```python
from cadgen import report, track, step
@step
def housing():
report("bearing housing") # name the current phase
for rib in track(ribs, label=lambda r: r.name): # count through a work list
...
```
`track()` advances the count when an item's work is DONE and labels the item in flight, so a
reader sees "3 finished, now on engines". The phase surfaces on the model's line in the build
tree and — through the daemon's job ledger — as `compiling · <phase>` in the CAD Viewer for any
document the job writes, whoever started the job. Without this a multi-minute assembly says
nothing during its longest phase.
**Failures** print the exception and the frames *in your own model*, not the runtime's:
```text
[cadgen] FAILED: ValueError: bad radius
[cadgen] src/widget.py:9 in bracket
[cadgen] return _profile(radius)
[cadgen] re-run with --verbose for the full traceback
```
A failed child raises at the site in the parent that first read its geometry, naming the call and carrying the child worker's output.
## Snapshots
**Snapshot inputs.** One format, one door, and the same `TARGET [OUT]` grammar `build` uses. `cadgen step snapshot` renders `.step`/`.stp` documents — nothing else (a model script is refused by name: run `python <model>.py`, then snapshot the STEP it wrote). A mesh file goes to its own door: `cadgen stl snapshot`, `cadgen 3mf snapshot`, `cadgen glb snapshot`. A mesh has no CAD topology, so the STEP-only options (`--focus`/`--hide`, `--display`, `--kinematics`, `--animation`/`--time`, `--mode section`) are not on those commands at all — check `--help` and the door tells you what it can do. Robot descriptions belong to the `urdf`/`srdf`/`sdf` skills. Each door refuses what is not its own format, and names the door that takes it.
```bash
cadgen step snapshot STEP/bracket.step tmp/review.png
cadgen stl snapshot STL/bracket.stl tmp/mesh.png
```
**Snapshot output.** The path you name is the path you get:
```bash
cadgen step snapshot STEP/bracket.step tmp/review.png
# then Read tmp/review.png
```
OUT is written exactly as given (a relative path against the current working directory), cleared before the render and written atomically after it — so reuse one name while iterating, name the iterations (`tmp/before.png`, `tmp/after.png`) when you need to compare, and treat a missing file as the failure signal: there is never an older image at the path to mistake for output. A directory (`tmp/`) is the don't-care case and gets a generated timestamped name inside it, printed on the `saved snapshot:` line. The same rule applies per output in a JSON packet.
**Theme and display.** Theme settings live under one `--theme`, display settings under one `--display` — the viewer's two tabs, one option each. The default theme is `snapshot`: Workbench Light with the ground grid and origin axis removed, because in a still image those read as geometry rather than as orientation. Pass `--theme workbench-light` for the viewer's own look. Projection is a theme trait honoured by every format, so a snapshot frames the same way the viewport does.
## Required workflow
Scale depth to the task: a simple part needs a short brief and few spec-driven checks; assemblies and fit-critical work need full positioning and alignment validation.
1. **Classify the task.** New part, new assembly, source modification, direct STEP/STP inspection, reference selection, measurement/alignment check, snapshot review, or mesh output request.
2. **Load only the needed references.** Use the triggers below instead of reading the whole reference set.
3. **Write a natural-language CAD brief.** Extract dimensions, units, coordinate convention, feature intent, output paths, assumptions, and validation targets from all provided inputs — prose, reference images, technical drawings. Use `references/cad-brief.md`.
4. **Check named purchasable components.** When an assembly includes named off-the-shelf actuators, servos, motors, electronics boards, connectors, or other purchasable components, search `$step-parts` before creating simplified placeholder geometry. If no exact match is found, record the miss and then use a documented bounding volume.
5. **Plan before coding.** Define the constants and factory arguments, intent labels, source paths, expected bounding boxes, and any mating/positioning datums before editing.
6. **Edit source, not generated artifacts.** Author a plain `.py` model script with one decorated function (shared code lives in plain helper modules; see `references/step-generation.md`). When a model script exists, run IT, never hand-edit its exported STEP. Imported STEP/STP files (no script) are handed straight to `cadgen step inspect`, `step snapshot` and the mesh doors — each compiles whatever it needs on demand.
7. **Build explicit targets.** Run each model script directly (`python <model>.py`); do not sweep directories. A parent builds its children as it calls them, so running the root is the whole build. Declare `@stl`/`@threemf`/`@glb` outputs on the model, or run `cadgen stl|3mf|glb build` for one-off mesh files. For multi-model project structure, read `references/project-layout.md`.
8. **Validate geometrically.** Run `cadgen step inspect refs <step-or-cad-target> --facts --planes --positioning` as the baseline, then verify the dimensions and relationships the user's spec calls out with targeted `measure`, `align`, `frame`, or `diff` checks. Run `cadgen step inspect validate <step-or-cad-target>` for geometry soundness: `refs --facts` reports counts and bounds, and its `ok` field covers ref resolution only — an open shell and an inverted solid both pass it.
9. **Snapshot the primary STEP — snapshot validation is mandatory.** After creating or visibly updating a STEP/STP part or assembly, ALWAYS run `cadgen step snapshot` against it and review the output; deterministic checks passing is not a reason to skip. The only skip cases are documented in `references/snapshot-review.md` (no visible geometry changed, or no valid artifact exists); report the reason when skipping. A mesh-only model is reviewed with its format's snapshot door.
10. **Repair and rerun.** If a check fails, change the smallest responsible source section, rebuild, and rerun the failed validation.
## Handoff
After completing CAD work that creates or modifies `.step`, `.stp`, `.stl`, `.3mf`, or native `.glb` artifacts, you must ALWAYS hand the explicit file path(s) to `$cad-viewer` when that skill is installed. `$cad-viewer` must start CAD Viewer if it is not already running and return link(s) to the relevant created or updated file(s); include those live viewer link(s) in the final response. If `$cad-viewer` is unavailable or startup fails, report that and rely on CLI inspection plus snapshots instead of silently omitting the handoff. This rule applies to every workflow in this skill, including mesh outputs.
When verification snapshots are generated, include the saved PNG snapshot(s) in the final response. If no snapshot applies, or if snapshot generation fails, say why and report the deterministic validation that still ran.
## Non-negotiables
- The model script is the source of truth. Every written file — STEP/STP, STL, 3MF, GLB, the sidecar — is a derived output; edit and rerun the script, never the outputs. Where a model declares a STEP, the STEP is the artifact that is inspected and snapshotted.
- Use named constants, closed solids, verbose native build123d labels, and source-controlled geometry intent.
- Author assembly positioning in source. `references/positioning.md` is authoritative for `AssemblyHelper`, build123d joints, explicit `Location` transforms, and alignment validation.
- Do not use `git status`, `git diff`, or file-size churn as CAD comparison for large exported STEP/STP, GLB, STL, or 3MF artifacts. Compare source changes, `cadgen step inspect` summaries, or snapshots instead; use path-limited git status only for bookkeeping.
- Report only checks that actually ran or are directly supported by tool output.
## Progressive references
Load these files only when their trigger applies:
- `references/cad-brief.md` — converting prose, reference images, and technical drawings into a CAD brief.
- `references/build123d-modeling.md` — build123d modeling patterns, topology, selectors, features, labels.
- `references/step-generation.md` — the model contract in full: composition (linked children, `read_step` inputs), what a rebuild tracks, mirrored parts, factories, the daemon and workers, imported STEP/STP files, and post-build steps.
- `references/inspection-and-validation.md` — validation sequence, selector refs, facts, planes, measurements, alignment, diff, frame, and validation reporting.
- `references/snapshot-review.md` — mandatory snapshot policy, packet sizing, targeted views, and converting visual findings into geometry checks.
- `references/positioning.md` — part-local datums and origins, assembly transforms, build123d joints, CLI alignment validation, and positioning reports.
- `references/kinematics.md` — articulating, posing, or animating a STEP model: typed mates (`kinematics=` on the decorators — mates, couplings, pose presets, export-at-pose), and the render module beside the document (`<name>.step.js`: the choreography contract, loaded by the viewer, read by no build).
- `references/supported-exports.md` — STL/3MF/native GLB outputs: declared exports, mesh-only models, and the `cadgen stl|3mf|glb build` doors.
- `references/repair-loop.md` — diagnosis and repair procedures.
- `references/project-layout.md` — project structure for anything bigger than a couple of loose models: `src/` for model scripts and shared code, format folders (`STEP/`, `DXF/`, `STL/`) for raw outputs, naming, and commit policy; `references/project-template.md` is the copyable exemplar. Read them when a project has more than a couple of models or when asked how to organise CAD code and artifacts.
- `references/migrations.md` — the tooling disagreeing with a model you believe is correct: recognizing a project authored against an older cadgen, and where the migration guides live.
Final responses should include generated files, returned `$cad-viewer` viewer links, verification snapshots, validation actually run, assumptions, and caveats. Use `references/inspection-and-validation.md` for report structure.