Agentic 3D Modeling
Overview
Produce parametric, 3D-printable models from a spec, verified by measurement, not by looking. The core discipline is the verify loop: every iteration renders to PNG and backs the claim with a number. A render that "looks solid" proves nothing — a hollow with no occlusion looks identical. Distilled from a series of real builds — caps, covers, cradles, enclosures, and stands (worked examples linked below).
Step 0 — pick the fabrication form before you model (don't default to a 3D solid)
"3D-printable solid" is not the default medium — defaulting there is an RL bias this skill counters (the "Year-1-student" trap: clever solids that don't need to be solid). But flatten-first is a check, not a new dogma — flat plates are strong in-plane, weak out of it. Triage before you extrude:
- Profile + uniform thickness? (bracket, link, arm, gusset) → prototype as a flat plate and
export_dxf alongside the STL — fast to iterate, re-cuttable in sheet stock. Wrong call when the part sees out-of-plane / torsional load, an eccentric load path, high bearing stress, a precision or high-cycle pivot, or becomes a tall cantilevered spacer-stack — there a boxed/printed solid or a proper bracket is stiffer and correct.
- Single-axis pivot? The real question is shear mode, not "fork vs flat." A 2-plate lap is single shear (screw bends, holes ovalize under cycling) — fine only for low-load / low-cycle. For loaded joints put the moving link in double shear: a center link between two outer plates — which is exactly a clevis, built flat. So don't fear the fork shape; only skip a bespoke 3D-printed fork when flat plates give the same double shear cheaper.
- Bearing + clamp, done right: a stamped washer is a sacrificial slip surface, not a bearing. For cyclic motion use a shoulder screw + bushing / sleeve / thrust washer, and let a spacer or shoulder set the clamp length so pivot friction doesn't depend on nut torque (the nyloc retains the stack, it doesn't set the swing).
- Reserve full 3D for what needs it: enclosures, organic shells, true 3D mating to an existing object (a motor boss, a connector).
Smell test: "special / hard-to-describe" shape ≈ over-solidified; nameable (plate, bar, bracket, link) is usually enough — but nameable ≠ adequate, re-check the load cases above.
DXF reality: ExportDXF(unit=Unit.MM); e.add_shape(sketch.sketch); e.write(path) emits the profile, but the same DXF is not drop-in across fabs — laser kerf/HAZ, waterjet taper, router bit-radius/dogbones, acrylic crazing at holes, ply grain, aluminium burrs each need their own allowance. Don't laser carbon fibre (toxic fumes, conductive dust, wrecked optics) — CF is routed or waterjet. Printed vertical through-holes dodge the horizontal-hole overhang but still print undersized/faceted → ream pivot holes; and counterbores / countersinks / hex-pockets are still overhangs.
Lived-experience reflexes (don't draft in a vacuum)
Failures an agent ships that a human in a workshop wouldn't — embodied knowledge that's in training but not reflexive. These apply when the model claims an assembly, a load path, motion, or fabrication-readiness — not to an isolated part you're deliberately drawing to a spec (a bracket to a bolt pattern, a spacer, a cover are fine in isolation, as long as the interfaces / datums / loads are stated).
- Nothing floats in a claimed assembly. If a part drives or carries load, name what fixtures it and where the reaction force goes — don't render it secured to nothing. The environment is often free fixturing + a free datum (a bench, a clamp, a table edge/underside) — consider it, but check it's actually stiff/flat/safe enough; it's an example, not a law.
- Static placement ≠ motion validation. Putting parts in position is not testing a mechanism. If the design depends on motion, define the moving DOF and sweep it through its range (sliders / animation / trace) — a single static pose hides clips, singularities, and binds. (In CAD a "mate" is an assembly constraint; many are deliberately static — the point is to exercise the DOFs, not that every mate moves.) Cross-in-projection is safe only if the swept solids clear — via separate axial planes or explicit relief (a slot, fork, jog, or non-overlapping range). Bars that overlap in the projected linkage — e.g. the two long arms of a narrow parallelogram, whose arcs sweep close enough to cross when drawn coplanar (a wide one keeps them apart, so it's geometry-dependent, not automatic) — bind if they share a plane with no relief; give each its own plane (or relieve it) and verify clearance with real part thickness, swept through the whole range, not as zero-width lines in one pose. A "works spatially" claim means the swept solids clear, not the stick figure doesn't cross.
- Separate the schematic from the part. A layout/kinematic schematic (beams, blocks) legitimately has meaningless overlaps — don't read printability off it. A manufacturable model must pass real checks: interference (
signed_distance / vertex-in-solid → ideally zero), bed-fit, seating, and overhang/support cost. The checks that can be numbers should be; printability isn't one scalar, and support-free is a goal, not a law.
- Keep the printer fed — with reviewed, low-risk jobs. If fabrication is the bottleneck and a candidate has passed its fit checks and is safe to run unattended, queue the smallest useful plate before downtime rather than leaving the bed idle. Not every model qualifies — don't speculatively print un-reviewed or stale revisions (that's a shoebox of obsolete plastic + overnight-failure risk, not progress). Machine utilisation ≠ engineering progress.
- One clamp path per screw-pivot — the bolt-axis stack. For a plain screw/bolt used as a pivot, exactly one member should provide retention/clamp (the self-tap, or the nut); members meant to rotate relative to the pin need a clearance hole plus an explicit bearing surface. Enumerate the stack at each pivot and count grips: two members both gripped onto a bolt they're meant to rotate on usually locks the joint — the intended revolute DOF is gone unless a separate shoulder / bushing / sleeve defines where rotation happens (a self-tap on both sides of an already-gripped bolt is the silent way to weld it). If the pin is deliberately fixed in multiple supports, name the bushing/shoulder the link turns on. Corollary — budget bolt length down the stack axis: if plate + boss + head eat the shank the thread never bites, so assert thread engagement ≥ your chosen minimum (e.g. ≥1×D or full nut height in metal — derate for soft / printed plastic, see Material realizability), not just that it "fits" in projection.
- Track each part's axial plane; close the stack-up against real contact faces. In a layered/offset-plane mechanism, hold which plane every part lives on. When a part mates to one on a different plane, something must span the axial gap between their contact faces — a standoff, step, shoulder, washer, or bearing stack — so assert the stack-up dimension (accounting for part thicknesses), not just the projected mate, and match the plane of parts already physically built so a new part joins the existing stack instead of floating a plane off it. A real datum-placed "mate" surfaces this gap for free; a stick-figure layout hides it.
- Record each interface's source of truth before inventing one. Before modeling anything that attaches to existing hardware, pin each interface to a source — a measured existing part, a registry part ID, a drawing, or an explicit stated assumption — and search existing parts / the registry for one that already exists, rather than building a substitute ("a tower") for a part that's on the bench or attaching to "nothing". Constraints the operator already stated — the fastener inventory, which side a cable exits, the plane a printed part sits on — are inputs to hold, not facts to silently re-derive, drop, or contradict (drawing 6 loose holes after "there are 4 nut-pairs" is the tell).
Toolchain — one shared CAD venv, owned by nobody's project
build123d 0.10 (Python + OpenCascade via cadquery-ocp) gives real fillets, true shell, and STEP (editable B-rep) and STL (mesh) from one source; plus trimesh (section + proximity), matplotlib (Agg, headless), numpy.
The interpreter is ~/.venvs/cad/bin/python. It is a domain toolbox, not a project — no CAD project owns it, so no CAD project's deletion or cleanup can take it away. Create it if absent (idempotent, ~2 min, ~900 MB):
# Guard on IMPORTABILITY, not on the binary existing — an interrupted create leaves
# bin/python behind, and a binary-only test would skip the install and "succeed" empty.
~/.venvs/cad/bin/python -c "import build123d,trimesh,numpy,shapely,matplotlib,rtree,scipy,networkx,lxml,fcl,OCP" 2>/dev/null || {
uv venv --python 3.12 ~/.venvs/cad
VIRTUAL_ENV=~/.venvs/cad uv pip install \
'build123d==0.10.0' 'trimesh==4.12.2' 'numpy==2.4.6' 'shapely==2.1.2' \
'matplotlib==3.10.9' 'rtree==1.4.1' 'networkx==3.6.1' 'scipy==1.17.1' \
'manifold3d==3.5.2' 'ezdxf==1.4.4' 'cadquery-ocp==7.8.1.1.post1' \
'embreex==4.4.0' 'python-fcl==0.7.0.11' 'lxml==6.1.1'
}
~/.venvs/cad/bin/python -c "import build123d,trimesh,fcl,OCP; print('cad venv ok')"
uv venv on an existing path re-creates it, so the repair branch is safe to re-run. Note the import names differ from the package names: python-fcl→fcl, cadquery-ocp→OCP.
The pins are load-bearing — do not drop them for "latest". Unpinned, this resolves today to build123d 0.11.1 / trimesh 5.0.0 / cadquery-ocp-novtk — two major bumps away from the stack every existing CAD script in the estate was written and verified against. Upgrading is a deliberate act: bump the pins, re-run the existing build*.py scripts, and diff the exported STEP/STL against the previous output (STEP embeds a FILE_NAME timestamp — strip that line before diffing) before you trust it.
Do NOT borrow another project's venv, and do NOT glob ~/*/venv for one — that was the old advice here and it was wrong twice over: it made every CAD project depend on an unrelated project staying alive (the estate's whole CAD toolchain hung off ~/oak-d-iot-75-cradle/venv, which is a finished IoT cradle project), and the glob happily lands on a brew-python venv that a brew upgrade python@3.12 dangles. uv venv pins uv-managed CPython, which takes that specific failure off the table — not every failure: the venv still symlinks into ~/.local/share/uv/python/cpython-3.12.11-*/, so a uv python prune, or restoring ~/.venvs/cad onto a node without that interpreter, still breaks it. The repair is always the create block above, never a hunt for a substitute venv. Same rule for any other domain toolbox (~/.venvs/web for playwright/selenium/PIL) — reach for ~/.venvs/<domain>, never a sibling project.
Why 3.12: cadquery-ocp has no wheel for bleeding-edge CPython (no cp314), so pin --python 3.12 rather than letting uv pick the newest. Never --break-system-packages. On a Linux homelab node the same recipe works unchanged (uv resolves its own 3.12; don't reach for /opt/homebrew, which is Apple-Silicon-only).
A project still owns its INPUTS. The venv is shared; the STLs/STEPs/frames a build script loads are not. trimesh.load('stl/front1.stl') reading from a session scratchpad is the same bug one layer down — a script that can't re-run from a clean checkout of its own directory is already broken. Commit the inputs next to the script, or generate them in it.
Exports are FREE FUNCTIONS, not methods: export_stl(shape, path, tolerance=.01, angular_tolerance=.1), export_step(shape, path). (shape.export_stl → AttributeError.) Units strictly mm; resolve mixed cm/mm in the spec up front.
Reach the empirical loop fast — the interpreter is your spatial scratchpad
You have no spatial workspace; holding the finished part in your head as prose is slow and error-prone — in-head reach/tolerance derivation is exactly where a dimension gets silently mis-computed (it's how the self-confirming bug below is born). Don't pre-simulate the whole part before writing code. Get the smallest runnable script — one solid, one cut, one print(solid.bounding_box()) — executing in the first minute, then let measurement, not mental imagery, carry the geometry forward. OCC can fail silently (empty booleans, no-op offsets, swallowed fillets); only running surfaces it, so a minute of python build.py beats ten of in-head deliberation. Leave the printed numbers and asserts in the script — they are the audit trail that makes a long build transparent to the next reader.
The verify loop (the core — render → section → MEASURE)
Each iteration: render PNG, Read it, and attach a number.
- Section, not 3D plot. matplotlib 3D (
Poly3DCollection) has no occlusion. Verify with a true 2D cross-section — B-rep edges, or trimesh.section(plane_origin, plane_normal) on the exported mesh — plus numeric probes.
faces().filter_by(Plane.XZ) misclassifies cut faces on lofted/tilted solids (empty section) → fall back to trimesh.section on the mesh.
- Make every constraint a falsifiable metric:
- fit/clearance (two bodies):
trimesh.proximity.signed_distance / closest_point; report min/max/mean/σ; want 0 penetration.
- wall thickness (one hollow body):
signed_distance against a watertight mesh of itself doesn't directly give it. Two ways: (a) take the trimesh.section at the cut and measure outer-edge → inner-edge distance numerically on the 2D section polygon; or (b) sample points on the outer surface (mesh.sample(n) filtered to outward-normal faces) and ray-cast inward (mesh.ray.intersects_location) — the first hit distance is the local wall. Report min/mean to confirm the intended thickness.
- retention: the cradle math below.
- printability: the overhang math below.
- Assert the volume drop after every boolean cut — the single highest-value check (catches wrong-direction extrudes, missed booleans).
- A check fed the value it's testing is not a test (self-confirming verification). Probe the as-built solid, never the nominal parameter or an unconfirmed assumption you fed in:
bb = solid.bounding_box(); assert abs(bb.max.X - R_OUT) < tol (tol ~1e-6 on a B-rep; looser on a mesh), read contact/reach off the mesh extent or trimesh.section, not off the constant. Burned once — a rounded nose Circle(FW/2) centred at the tip radius put real material FW/2 beyond it; the verify checked the tip radius and certified a part whose true reach ran 8 mm into the keep-out it was meant to clear. Same trap for guessed interfaces (a bolt PCD, an across-flats): state the assumption to the operator up front — a part that "verifies" against its own guess proves nothing.
- The assembly-level twin — same trap, one level up (verify the ASSEMBLY, not just each part). Placing each part by the very pivot you're checking is feeding a check its own answer, spatially — the render then cannot disagree, so a hole mismatch is invisible by construction. Burned live: an assembly render that mapped each part's assumed local hole coords onto nominal kinematic pivots (
place(part,(0,0),(120,0), world_M, world_E)) drew a clean leg whose real mating holes never lined up; part-by-part checks were all green, and the operator only caught it by printing. In a claimed assembly, place every part by an independent datum (the fixed frame, or the mating part's datum — never the hole under test), load the as-built mesh (not ideal points), and assert per pin: mating-hole coaxiality (axis-to-axis distance ≈0), correct axial plane, and one clamp path per screw-pivot (above, under Lived-experience reflexes). Use a rotatable/sweepable assembly view or a trimesh.section through the joint — a static, self-placed plot cannot expose this class of mismatch. If the operator can only find a spatial fault by printing, the verification substrate failed, not the printer.
- Render gravity-aligned views (y=0 side cut + ⟂-axis cross-section, gravity up), not raw point clouds.
- After any parameter change, re-grep artifacts for hardcoded old values (plot titles, comments) — a stale caption on correct geometry is a misleading deliverable.
Material realizability — verify the part can EXIST in its material, not just its geometry
The verify loop proves the shape; it says nothing about whether the material can be made into that shape and survive its load. A hole can be exactly Ø12, watertight, with a clean taper — and still be impossible to use, because that Ø is the hole for a steel tap to cut an M14 thread, not for a plastic part to have one formed into it. Burned exactly there: an M14×2 self-tap hole modelled at Ø12, "verified" (hole = 12.0, watertight, taper present), declared good. But M14×2 is basic minor ≈ 11.8, cutting tap-drill ≈ 12.0, pitch-Ø ≈ 12.7, major 14.0 — Ø12 is the tap-drill, the hole you CUT a full thread into with a steel tap (shop rule tap-drill ≈ major − pitch ≈ 75% thread; the basic minor is a touch under — Yamawa / CustomPartNet). Forming that full thread with a plain machine bolt in FDM PLA means displacing ~1 mm of material radially = max torque → it strips/cracks. Geometry verified, material un-checked, part ships broken.
So before declaring done, run this pass alongside the verify loop — for any feature that deforms or loads the material, the falsifiable check + a number (every number is material+process dependent — state which up front):
- Self-tap / thread-form (plain bolt into plastic): pilot no smaller than pitch-Ø, target nearer major — and TEST. A plain 60° bolt should form ≤ ~0.3–0.5 mm radial in PLA, which on M14 (major 14) means pilot ~13–13.5, not 12.0 (and even pitch-Ø 12.7 is ~0.65 mm radial — already marginal). More than that strips. Forming displaces, so the hole runs looser than the cutting tap-drill (form-taps run ~5–10% over the cutting drill — engineerfix / accu-components). Tap-drill ≈ minor is for a tap to cut, never for a bolt to form. Dedicated thread-forming screws (relief on the screw) are quoted ~0.8 × major (EJOT Delta PT / Semblex) — but that's molded-boss/screw-specific and can still crack in PLA, so print a pilot ladder and find the size that holds rather than trusting one number. Add a tapered lead-in to start it.
- Press-fit / interference (FDM): radial interference ~0.05–0.10 mm/side (≤0.2 mm total), PLA at the low end, PETG slightly more — not ~1 mm (10–20× too much → cracks). Relief slots / split bores cut the required stretch. (AON3D / 3DPut fit guides.)
- Snap-fit / living hinge / cantilever: keep peak bending strain under the material's allowable single-assembly strain — ABS ~6–7%, Nylon-6 ~8.8%, brittle PET ~2% (BASF Snap-Fit Manual); design to ≤50% of yield strain for amorphous (PC/ABS), ≤70% for semi-crystalline (nylon). These are MOLDED-plastic values — derate hard for FDM (layer adhesion, notch at the root, raster voids cut the real allowable). PLA is brittle — treat it PET-class (~1–2%), not nylon-class (extrapolated — prototype it). A PLA arm sized for nylon's strain snaps off; living hinges want PP/nylon, never PLA.
- Anisotropy (the orientation check): FDM inter-layer (Z) tensile strength is only ~50% of in-plane (real spread 30–75%, material/param-dependent — RapidMade / FDM interlayer studies). Don't load inter-layer bonds in tension — orient so tensile/bending load runs along layers / through solid filament, not pulling layers apart. A boss, snap arm, or screw lug loaded across the layer lines fails at a fraction of its modelled strength.
Rough properties that set the numbers above (datasheet/molded reference — printed values run lower and print-settings/orientation often dominate, so use the filament TDS): PLA ~3.5 GPa / ~50 MPa, brittle; PETG ~2.2 / ~53, ductile; ABS ~2.0 / ~41, ductile; Nylon ~3.3 / ~86 (that's dry molded PA66 — printed PA6/PA12 is lower and hygroscopic), ductile; CF-nylon stiff but strongly anisotropic (XY bending modulus ~10 GPa vs Z ~3 GPa, product-specific) (Prusa / Bambu TDS / superjamie filament wiki). A fit "verified" against an unstated material proves nothing — the self-confirming-verification trap, one layer down.
Geometry gotchas (each hit and solved)
- Winding sets extrude direction.
extrude(sketch, +h) follows the face normal; a clockwise-wound Polygon normals −Z → extrudes downward, and a CW cut profile silently misses the boolean (symptom: volume barely drops). Rectangle/Trapezoid helpers are CCW (safe); hand-built Polygon([...]) is the trap — list points CCW. Assert the volume drop.
- OCC 3D offset dies on a
scale()d sphere (Standard_Failure: BRep_API: command not done, degenerate pole/seam). Build domes as a loft of stacked ellipse sections instead — also yields a vertical equator tangent that blends into a straight wall.
- Uniform-wall open cap:
offset(solid,+t) − solid, then split flat at the rim plane → sharp planar edges → fillet inner only. Avoid Kind.ARC/Kind.INTERSECTION on curved offsets when an outer edge must stay sharp.
- Fillet radius ≥ wall thickness consumes the whole wall — cap it (e.g. 0.5 mm on a 1 mm wall) and flag the judgment call.
- Don't blanket-mirror features. A Y-mirror of an X-symmetric profile looks like a 180° rotation — flips chamfer/keying. Mirror only features referenced to the moved edge; edge-referenced features ("5 mm from wide edge") are flip-invariant and must NOT be mirrored (mirroring = regression).
- Vertical truncation cut: to amputate a region of a tilted part cleanly, subtract a world-z half-space box, not the irregular feature boundary.
Anchor first — solve load-bearing geometry before details
For "what angle does it rest at": find the natural tilt by minimizing CG height vs lean angle — rotate the mesh through θ, compute cg_z − min_vertex_z (CG height after dropping to ground), take the local min. Don't hand-derive contacts. Then ENFORCE that pose by building the seat at it — the part fixes the angle, robust to unknown CG/weight (weight only affects the stability margin).
Cradle retention math (snap-capture a cylinder)
A trough retains a cylinder only if opening width < cylinder diameter:
opening = 2·(r + clearance)·cos(wrap/2 − 90) must be < 2r.
clearance = the print gap you cut the trough oversize by, in mm, positive (typical FDM 0.2–0.4 mm so the rod actually inserts; 0 only for a theoretical check). A bigger clearance widens the opening → harder to retain, so size wrap against the clearance you'll actually print.
- Angle convention: degrees. The
90 is degrees and wrap is in degrees. In code, math.cos takes radians — convert: math.cos(math.radians(wrap/2 - 90)). A literal math.cos(wrap/2 - 90) is silently wrong.
- Needs wrap > 180° by a real margin. 190° ≈ 99.6% of dia = no retention (lifts straight out). ~220° ≈ 1 mm constriction, grips ~67% of height = real snap-fit (press in with slight wall flex).
- "Grip >50% of height" = lips past the equator; height gripped
= r·(1 + sin(wrap/2 − 90)).
Printability metric (FDM, support-free)
Per face: if it faces down (normal_z < 0) AND is above the bed, surface incline from horizontal = arccos(|normal_z|); needs support if < ~45°. CRITICAL: exclude bed-contact faces (centroid_z < ~0.6 mm) or the flat bottom false-flags as a 0°-overhang ceiling (it's the first layer). Design tricks: print top-plate-down for caps; a 0.2 mm membrane closing a counterbore floor lets the printer bridge a flat instead of an overhang ledge (drill through after).
Warp-awareness (FDM — the big flat base that curls off the plate)
Warping is a printability failure the CAD model controls, the same way support-free is — a slicer brim only fights what the geometry invites. A large contiguous flat bottom accumulates differential-shrinkage stress that bows the first layers upward (sharp corners lift first, but a rounded slab still bows); on a big part it can lift hard enough to peel the whole print off a magnetic plate mid-job (a taco'd base, not a cosmetic blemish). Design it out — biggest lever first:
- Kill contiguous flat-bottom area — skeletonize a large base. Warp force scales with the first-layer contiguous area, so a solid slab is the worst case. Replace it with a perimeter rail + cross-ribs + solid pads only under the load points (walls, bosses, mounts) and open windows between. The original part you're copying often already has these cut-outs — keep them, don't "helpfully" fill them in. Cuts warp, plastic, and print time at once.
- Round the footprint corners, chamfer the bottom edge. Corners are the stress concentrators and lift first; a fillet (R ≥ ~8 mm) on the outline plus a small bottom-edge chamfer removes most corner-curl.
- Design-in mouse-ears / brim tabs at the corners (thin snap-off discs) instead of leaning on the slicer's brim — the anchor is then part of the model and survives a re-slice / a different operator.
- Split an oversized flat part. Two smaller plates warp far less than one big one — corner-lift grows fast with span (a free differential-shrinkage strip bows as span², an upper bound; bed adhesion softens the real curve but the direction holds), so halving the span helps super-linearly. Bolt or dowel them. A ~200 mm solid footprint is already a warp risk worth splitting or skeletonising.
- Material is a multiplier, not the cause. ABS/PETG/CF/PA warp several× more than PLA; a big flat base in a warpy filament is a red flag — surface it to the operator and lean harder on the levers above.
Falsifiable check (fold into the verify loop) — gate on contiguity FIRST; corners aggravate, they aren't required. From the as-built mesh (never the nominal params): take bed-contact faces (fc[:,2] < ~0.6 and fn[:,2] < -0.5), sum their contact_area, and take the footprint bbox of those triangles. Flag when the footprint is large (max side > ~150 mm) and contiguous (contact_area / (fx*fy) > ~0.6 — a solid slab ≈ 1.0; a skeletonised rail-and-ribs base drops well below). Don't require sharp corners — rounding fixes corner-curl, not the global bow, so a large rounded slab must still flag (battle-tested: an R8-filleted 235 mm slab clears any corner test yet is still contiguity ≈ 1.0 → warps). Score corner-sharpness only as a severity aggravator, via a morphological opening of the footprint outline (poly.buffer(-R).buffer(R), R≈8; the removed area is the sharp-corner material) — apply it after the contiguity gate, since a raw opening also eats thin ribs. Recommend skeletonise + fillet before the part ever prints; don't wait for the operator's photo of a base that peeled the plate. (The ~150 mm / R8 / 0.6 numbers are one-incident + first-principles — they discriminate a slab from its fixes in a synthetic sweep, but pin them against real prints before trusting the exact values.)
Reverse-engineer geometry from photos (no calipers)
- Credit card = free scale ruler. ISO/IEC 7810 ID-1 = 85.60×53.98 mm; threshold its color +
scipy.ndimage.label for px→mm (check X-vs-Y anisotropy = perspective).
- Known hole pattern = free homography anchor. A Pi-stackable board uses 58×49 mm, Ø2.7 (M2.5), 3.5 mm inset; those 4 holes give an exact 4-point DLT homography (numpy) → rectify to mm, read outline + connector X-positions. Classical CV (scipy blobs + DLT) beats YOLO here — deterministic, sub-mm, no training set.
- What a photo CANNOT give: connector heights above the PCB → look up by part type (KF301 ~10, DC barrel ~11, JST XH ~6 / PH ~5, Type-C ~3.2, 2.54 header ~8.5 mm) or caliper on arrival. This is why a SAFE variant (walls open to rim, height-tolerant) is worth carrying alongside the tight BEAUTIFUL one.
- 1:1 PDF for physical overlay:
figsize=(w_mm/25.4, h_mm/25.4), ax=fig.add_axes([0,0,1,1]), data limits = part extent, aspect equal, NO bbox_inches='tight' (it rescales!). Add a 10 mm verify-bar; user prints at 100% (no fit-to-page), overlays the real part, reports per-axis residuals (screen overlay gives per-axis, not uniform, scale error — correct each axis independently; handedness/flip is the #1 confusion).
Workflow patterns
- Parametric, one source. Knobs as constants at the top; a dimensional variant is a one-line change or a
VARIANTS list, not a forked file.
- Emit BOTH variants, never overwrite one file —
VARIANTS=[("cap_dev",False),("cap_sleek",True)] writes both .step/.stl. Overwriting a single cap.stl lost a good cap once.
- Driving Bambu Studio to slice: see memory
bambu-studio-computer-use (native file dialogs are invisible to screenshots → open -a BambuStudio file.stl).
Common mistakes
| Mistake |
Fix |
| Defaulting to a 3D solid without checking if it's a plate problem |
Step 0 triage; flat plate for in-plane loads, double-shear + bushing for loaded pivots (not a single-shear washer-lap), export_dxf for sheet |
| "It renders solid" as proof |
3D plots have no occlusion — section + a number |
| A metric computed from the input parameter / a guessed dimension |
Self-confirming, not falsifying — probe the as-built solid (assert bounding_box().max ≈ expected); state guessed interfaces to the operator |
| Verifying a feature's geometry but not whether it can EXIST in its material (self-tap at the tap-drill Ø, ~1 mm press-fit, PLA snap arm, load across layer lines) |
Material-realizability pass — pilot ≈ pitch-Ø not tap-drill; press-fit ≤0.2 mm; strain under the material's allowable; don't load inter-layer (Z) in tension |
Borrowing another project's venv, or globbing ~/*/venv for one |
Use ~/.venvs/cad/bin/python (create per Toolchain §). A project-owned venv rotates and dies with its project — cap-model/venv was deleted by a cleanup sweep, and the whole CAD estate once hung off a finished IoT project's brew-python venv |
shape.export_stl(...) |
Exports are free functions: export_stl(shape, path) |
CW-wound Polygon cut silently misses |
List points CCW; assert volume drop per cut |
offset() on a scale()d sphere crashes |
Loft stacked ellipse sections for domes |
| Blanket-mirroring all features |
Mirror only moved-edge-referenced ones |
| Flat print-bed face flagged as overhang |
Exclude centroid_z < ~0.6 mm from the metric |
| Trusting a photo for connector heights |
Photo gives X/Y only; heights from part-type lookup → SAFE variant |
| Shipping a large solid flat base (big contiguous first-layer footprint; sharp corners worsen it but aren't required) → warps/peels off the plate |
Gate on contiguity first (contact_area/bbox > ~0.6 on a >~150 mm footprint) — skeletonise (perimeter rail + ribs + pads, open windows), fillet corners + chamfer bottom edge, design-in mouse-ears; split if huge; warpy material = red flag (see Warp-awareness) |
| Verifying each part in isolation, never the assembly (parts placed by the hole under test) |
Place by an independent datum; assert mating-hole coaxiality + axial stack-up + one-clamp-per-screw-pivot; rotatable/section render, not a self-placed static plot |
| Self-tapping/nutting both rotating members of one pivot, or two crossing bars sharing a plane |
One clamp path per screw-pivot (a shoulder/bushing bears if the pin is multiply fixed); crossing bars on different planes or with relief, swept with real thickness |
Worked examples
Core builds, one per technique this skill leans on:
- agentic-cad-fan-stand — cradle retention and rest-angle anchoring, every constraint checked as a number
- gimbal-base-cover — support-free printability: teardrop holes, 45° buttressed bosses
- connector-cap — reverse-engineering a part from photos when the unit ships no CAD
- f95d-21700-charger — the verify loop turned on someone else's file: a fan housing cut down to a charger, with the electronics bay proven identical to the donor
Other examples in the evnchn-agentic org if you need a different angle.
1---2name: agentic-3d-modeling3description: Agentic 3D Modeling4---56# Agentic 3D Modeling78## Overview910Produce parametric, 3D-printable models from a spec, **verified by measurement, not by looking**. The core discipline is the verify loop: every iteration renders to PNG **and backs the claim with a number**. A render that "looks solid" proves nothing — a hollow with no occlusion looks identical. Distilled from a series of real builds — caps, covers, cradles, enclosures, and stands (worked examples linked below).1112## Step 0 — pick the fabrication form before you model (don't default to a 3D solid)1314"3D-printable solid" is **not** the default medium — defaulting there is an RL bias this skill counters (the "Year-1-student" trap: clever solids that don't need to be solid). But **flatten-first is a *check*, not a new dogma** — flat plates are strong in-plane, weak out of it. Triage before you extrude:1516- **Profile + uniform thickness?** (bracket, link, arm, gusset) → prototype as a **flat plate** and `export_dxf` alongside the STL — fast to iterate, re-cuttable in sheet stock. **Wrong call when the part sees out-of-plane / torsional load, an eccentric load path, high bearing stress, a precision or high-cycle pivot, or becomes a tall cantilevered spacer-stack** — there a boxed/printed solid or a proper bracket is stiffer and correct.17- **Single-axis pivot?** The real question is **shear mode, not "fork vs flat."** A 2-plate lap is **single shear** (screw bends, holes ovalize under cycling) — fine only for low-load / low-cycle. For loaded joints put the moving link in **double shear**: a center link between two outer plates — which is *exactly a clevis, built flat*. So don't fear the fork *shape*; only skip a bespoke **3D-printed** fork when flat plates give the same double shear cheaper.18- **Bearing + clamp, done right:** a stamped washer is a sacrificial slip surface, **not a bearing**. For cyclic motion use a **shoulder screw + bushing / sleeve / thrust washer**, and let a **spacer or shoulder set the clamp length so pivot friction doesn't depend on nut torque** (the nyloc *retains the stack*, it doesn't set the swing).19- **Reserve full 3D** for what needs it: enclosures, organic shells, true 3D mating to an existing object (a motor boss, a connector).2021**Smell test:** "special / hard-to-describe" shape ≈ over-solidified; *nameable* (plate, bar, bracket, link) is usually enough — but **nameable ≠ adequate**, re-check the load cases above.2223**DXF reality:** `ExportDXF(unit=Unit.MM); e.add_shape(sketch.sketch); e.write(path)` emits the profile, but the **same DXF is not drop-in across fabs** — laser kerf/HAZ, waterjet taper, router bit-radius/dogbones, acrylic crazing at holes, ply grain, aluminium burrs each need their own allowance. **Don't laser carbon fibre** (toxic fumes, conductive dust, wrecked optics) — CF is routed or waterjet. Printed vertical through-holes dodge the *horizontal*-hole overhang but still print undersized/faceted → **ream pivot holes**; and counterbores / countersinks / hex-pockets are still overhangs.2425## Lived-experience reflexes (don't draft in a vacuum)2627Failures an agent ships that a human in a workshop wouldn't — embodied knowledge that's in training but not reflexive. These apply when the model **claims an assembly, a load path, motion, or fabrication-readiness** — *not* to an isolated part you're deliberately drawing to a spec (a bracket to a bolt pattern, a spacer, a cover are fine in isolation, as long as the interfaces / datums / loads are stated).2829- **Nothing floats in a claimed assembly.** If a part drives or carries load, name what fixtures it and where the reaction force goes — don't render it secured to nothing. The environment is often free fixturing + a free datum (a bench, a clamp, a table edge/underside) — consider it, but check it's actually stiff/flat/safe enough; it's an example, not a law.30- **Static placement ≠ motion validation.** Putting parts in position is not testing a mechanism. If the design depends on motion, define the moving DOF and **sweep it through its range** (sliders / animation / trace) — a single static pose hides clips, singularities, and binds. (In CAD a "mate" is an assembly *constraint*; many are deliberately static — the point is to *exercise* the DOFs, not that every mate moves.) **Cross-in-projection is safe only if the swept solids clear** — via separate axial planes or explicit relief (a slot, fork, jog, or non-overlapping range). Bars that overlap in the *projected* linkage — e.g. the two long arms of a *narrow* parallelogram, whose arcs sweep close enough to cross when drawn coplanar (a wide one keeps them apart, so it's geometry-dependent, not automatic) — **bind if they share a plane with no relief**; give each its own plane (or relieve it) and verify clearance with **real part thickness, swept through the whole range**, not as zero-width lines in one pose. A "works spatially" claim means *the swept solids clear*, not *the stick figure doesn't cross*.31- **Separate the schematic from the part.** A layout/kinematic schematic (beams, blocks) legitimately has meaningless overlaps — don't read printability off it. A *manufacturable* model must pass real checks: interference (`signed_distance` / vertex-in-solid → ideally zero), bed-fit, seating, and overhang/support cost. The checks that can be numbers should be; printability isn't one scalar, and support-free is a goal, not a law.32- **Keep the printer fed — with reviewed, low-risk jobs.** If *fabrication* is the bottleneck and a candidate has passed its fit checks and is safe to run unattended, queue the smallest useful plate before downtime rather than leaving the bed idle. Not every model qualifies — don't speculatively print un-reviewed or stale revisions (that's a shoebox of obsolete plastic + overnight-failure risk, not progress). Machine utilisation ≠ engineering progress.33- **One clamp path per screw-pivot — the bolt-axis stack.** For a plain screw/bolt used *as* a pivot, exactly one member should provide retention/clamp (the self-tap, or the nut); members meant to rotate relative to the pin need a **clearance** hole plus an explicit bearing surface. Enumerate the stack at each pivot and count grips: **two members both gripped onto a bolt they're meant to rotate on usually locks the joint** — the intended revolute DOF is gone *unless a separate shoulder / bushing / sleeve defines where rotation happens* (a self-tap on *both* sides of an already-gripped bolt is the silent way to weld it). If the pin is deliberately fixed in multiple supports, **name the bushing/shoulder** the link turns on. Corollary — **budget bolt length down the stack axis:** if plate + boss + head eat the shank the thread never bites, so **assert thread engagement ≥ your chosen minimum** (e.g. ≥1×D or full nut height in metal — derate for soft / printed plastic, see Material realizability), not just that it "fits" in projection.34- **Track each part's axial plane; close the stack-up against real contact faces.** In a layered/offset-plane mechanism, hold *which plane every part lives on*. When a part mates to one on a **different** plane, *something* must span the axial gap between their contact faces — a **standoff, step, shoulder, washer, or bearing stack** — so **assert the stack-up dimension** (accounting for part thicknesses), not just the projected mate, and **match the plane of parts already physically built** so a new part joins the existing stack instead of floating a plane off it. A real datum-placed "mate" surfaces this gap for free; a stick-figure layout hides it.35- **Record each interface's source of truth before inventing one.** Before modeling anything that attaches to existing hardware, pin each interface to a source — **a measured existing part, a registry part ID, a drawing, or an explicit stated assumption** — and **search existing parts / the registry** for one that already exists, rather than building a substitute ("a tower") for a part that's on the bench or attaching to "nothing". Constraints the operator already stated — the fastener inventory, which side a cable exits, the plane a printed part sits on — are **inputs to hold**, not facts to silently re-derive, drop, or contradict (drawing 6 loose holes after "there are 4 nut-pairs" is the tell).3637## Toolchain — one shared CAD venv, owned by nobody's project3839build123d 0.10 (Python + OpenCascade via `cadquery-ocp`) gives real fillets, true shell, and STEP (editable B-rep) **and** STL (mesh) from one source; plus `trimesh` (section + proximity), `matplotlib` (Agg, headless), numpy.4041**The interpreter is `~/.venvs/cad/bin/python`.** It is a domain toolbox, not a project — no CAD project owns it, so no CAD project's deletion or cleanup can take it away. Create it if absent (idempotent, ~2 min, ~900 MB):42```bash43# Guard on IMPORTABILITY, not on the binary existing — an interrupted create leaves44# bin/python behind, and a binary-only test would skip the install and "succeed" empty.45~/.venvs/cad/bin/python -c "import build123d,trimesh,numpy,shapely,matplotlib,rtree,scipy,networkx,lxml,fcl,OCP" 2>/dev/null || {46 uv venv --python 3.12 ~/.venvs/cad47 VIRTUAL_ENV=~/.venvs/cad uv pip install \48 'build123d==0.10.0' 'trimesh==4.12.2' 'numpy==2.4.6' 'shapely==2.1.2' \49 'matplotlib==3.10.9' 'rtree==1.4.1' 'networkx==3.6.1' 'scipy==1.17.1' \50 'manifold3d==3.5.2' 'ezdxf==1.4.4' 'cadquery-ocp==7.8.1.1.post1' \51 'embreex==4.4.0' 'python-fcl==0.7.0.11' 'lxml==6.1.1'52}53~/.venvs/cad/bin/python -c "import build123d,trimesh,fcl,OCP; print('cad venv ok')"54```55`uv venv` on an existing path re-creates it, so the repair branch is safe to re-run. Note the import names differ from the package names: `python-fcl`→`fcl`, `cadquery-ocp`→`OCP`.56**The pins are load-bearing — do not drop them for "latest".** Unpinned, this resolves today to build123d 0.11.1 / trimesh 5.0.0 / `cadquery-ocp-novtk` — two major bumps away from the stack every existing CAD script in the estate was written and verified against. Upgrading is a deliberate act: bump the pins, re-run the existing `build*.py` scripts, and diff the exported STEP/STL against the previous output (STEP embeds a `FILE_NAME` timestamp — strip that line before diffing) before you trust it.57**Do NOT borrow another project's venv, and do NOT glob `~/*/venv` for one** — that was the old advice here and it was wrong twice over: it made every CAD project depend on an unrelated project staying alive (the estate's whole CAD toolchain hung off `~/oak-d-iot-75-cradle/venv`, which is a *finished IoT cradle project*), and the glob happily lands on a **brew-python** venv that a `brew upgrade python@3.12` dangles. `uv venv` pins uv-managed CPython, which takes that specific failure off the table — not every failure: the venv still symlinks into `~/.local/share/uv/python/cpython-3.12.11-*/`, so a `uv python` prune, or restoring `~/.venvs/cad` onto a node without that interpreter, still breaks it. **The repair is always the create block above, never a hunt for a substitute venv.** Same rule for any *other* domain toolbox (`~/.venvs/web` for playwright/selenium/PIL) — reach for `~/.venvs/<domain>`, never a sibling project.5859**Why 3.12:** `cadquery-ocp` has **no wheel for bleeding-edge CPython** (no cp314), so pin `--python 3.12` rather than letting uv pick the newest. Never `--break-system-packages`. On a Linux homelab node the same recipe works unchanged (uv resolves its own 3.12; don't reach for `/opt/homebrew`, which is Apple-Silicon-only).6061**A project still owns its INPUTS.** The venv is shared; the STLs/STEPs/frames a build script loads are not. `trimesh.load('stl/front1.stl')` reading from a session scratchpad is the same bug one layer down — a script that can't re-run from a clean checkout of its own directory is already broken. Commit the inputs next to the script, or generate them in it.6263**Exports are FREE FUNCTIONS, not methods:** `export_stl(shape, path, tolerance=.01, angular_tolerance=.1)`, `export_step(shape, path)`. (`shape.export_stl` → AttributeError.) **Units strictly mm**; resolve mixed cm/mm in the spec up front.6465## Reach the empirical loop fast — the interpreter is your spatial scratchpad6667You have no spatial workspace; holding the finished part in your head as prose is slow **and** error-prone — in-head reach/tolerance derivation is exactly where a dimension gets silently mis-computed (it's how the self-confirming bug below is born). Don't pre-simulate the whole part before writing code. Get the smallest runnable script — one solid, one cut, one `print(solid.bounding_box())` — executing in the first minute, then let measurement, not mental imagery, carry the geometry forward. OCC *can* fail silently (empty booleans, no-op offsets, swallowed fillets); only *running* surfaces it, so a minute of `python build.py` beats ten of in-head deliberation. Leave the printed numbers and asserts in the script — they are the audit trail that makes a long build transparent to the next reader.6869## The verify loop (the core — render → section → MEASURE)7071Each iteration: render PNG, `Read` it, **and** attach a number.7273- **Section, not 3D plot.** matplotlib 3D (`Poly3DCollection`) has no occlusion. Verify with a true **2D cross-section** — B-rep edges, or `trimesh.section(plane_origin, plane_normal)` on the exported mesh — plus numeric probes.74- **`faces().filter_by(Plane.XZ)` misclassifies cut faces on lofted/tilted solids** (empty section) → fall back to `trimesh.section` on the mesh.75- **Make every constraint a falsifiable metric:**76 - *fit/clearance (two bodies):* `trimesh.proximity.signed_distance` / `closest_point`; report min/max/mean/σ; want 0 penetration.77 - *wall thickness (one hollow body):* `signed_distance` against a watertight mesh of itself doesn't directly give it. Two ways: (a) take the `trimesh.section` at the cut and measure outer-edge → inner-edge distance numerically on the 2D section polygon; or (b) sample points on the outer surface (`mesh.sample(n)` filtered to outward-normal faces) and ray-cast inward (`mesh.ray.intersects_location`) — the first hit distance is the local wall. Report min/mean to confirm the intended thickness.78 - *retention:* the cradle math below.79 - *printability:* the overhang math below.80- **Assert the volume drop after every boolean cut** — the single highest-value check (catches wrong-direction extrudes, missed booleans).81- **A check fed the value it's testing is not a test (self-confirming verification).** Probe the *as-built* solid, never the nominal parameter or an unconfirmed assumption you fed in: `bb = solid.bounding_box(); assert abs(bb.max.X - R_OUT) < tol` (`tol` ~1e-6 on a B-rep; looser on a mesh), read contact/reach off the mesh extent or `trimesh.section`, not off the constant. Burned once — a rounded nose `Circle(FW/2)` *centred* at the tip radius put real material `FW/2` beyond it; the verify checked the tip radius and certified a part whose true reach ran 8 mm into the keep-out it was meant to clear. Same trap for guessed interfaces (a bolt PCD, an across-flats): state the assumption to the operator up front — a part that "verifies" against its own guess proves nothing.82 - **The assembly-level twin — same trap, one level up (verify the ASSEMBLY, not just each part).** Placing each part *by the very pivot you're checking* is feeding a check its own answer, spatially — the render then **cannot** disagree, so a hole mismatch is invisible **by construction**. Burned live: an assembly render that mapped each part's *assumed* local hole coords onto *nominal* kinematic pivots (`place(part,(0,0),(120,0), world_M, world_E)`) drew a clean leg whose real mating holes never lined up; part-by-part checks were all green, and the operator only caught it by **printing**. In a **claimed assembly**, place every part by an **independent datum** (the fixed frame, or the mating part's datum — never the hole under test), load the **as-built mesh** (not ideal points), and assert per pin: **mating-hole coaxiality** (axis-to-axis distance ≈0), correct **axial plane**, and **one clamp path per screw-pivot** (above, under Lived-experience reflexes). Use a **rotatable/sweepable assembly view or a `trimesh.section` through the joint** — a static, self-placed plot cannot expose this class of mismatch. **If the operator can only find a spatial fault by printing, the verification substrate failed, not the printer.**83- Render gravity-aligned views (y=0 side cut + ⟂-axis cross-section, gravity up), not raw point clouds.84- After any parameter change, **re-grep artifacts** for hardcoded old values (plot titles, comments) — a stale caption on correct geometry is a misleading deliverable.8586## Material realizability — verify the part can EXIST in its material, not just its geometry8788The verify loop proves the *shape*; it says nothing about whether the material can be **made into** that shape and survive its load. A hole can be exactly Ø12, watertight, with a clean taper — and still be impossible to use, because that Ø is the hole for a steel tap to **cut** an M14 thread, not for a plastic part to have one **formed** into it. **Burned exactly there:** an M14×2 self-tap hole modelled at Ø12, "verified" (hole = 12.0, watertight, taper present), declared good. But M14×2 is basic minor ≈ 11.8, cutting tap-drill ≈ 12.0, pitch-Ø ≈ 12.7, major 14.0 — **Ø12 is the tap-drill, the hole you CUT a full thread into with a steel tap** (shop rule tap-drill ≈ major − pitch ≈ 75% thread; the *basic* minor is a touch under — *Yamawa / CustomPartNet*). Forming that full thread with a plain machine bolt in FDM PLA means displacing ~1 mm of material radially = max torque → it strips/cracks. Geometry verified, material un-checked, part ships broken.8990So **before declaring done**, run this pass alongside the verify loop — for any feature that **deforms or loads** the material, the falsifiable check + a number (every number is material+process dependent — state which up front):9192- **Self-tap / thread-form (plain bolt into plastic):** pilot **no smaller than pitch-Ø, target nearer major — and TEST.** A plain 60° bolt should form **≤ ~0.3–0.5 mm radial** in PLA, which on M14 (major 14) means **pilot ~13–13.5**, *not* 12.0 (and even pitch-Ø 12.7 is ~0.65 mm radial — already marginal). More than that strips. Forming displaces, so the hole runs **looser** than the cutting tap-drill (form-taps run ~5–10% over the cutting drill — *engineerfix / accu-components*). Tap-drill ≈ minor is for a tap to *cut*, never for a bolt to *form*. Dedicated **thread-forming screws** (relief on the screw) are quoted ~0.8 × major (*EJOT Delta PT / Semblex*) — but that's molded-boss/screw-specific and can still crack in PLA, so **print a pilot ladder and find the size that holds** rather than trusting one number. Add a tapered lead-in to start it.93- **Press-fit / interference (FDM):** radial interference **~0.05–0.10 mm/side (≤0.2 mm total)**, PLA at the low end, PETG slightly more — **not ~1 mm** (10–20× too much → cracks). Relief slots / split bores cut the required stretch. (*AON3D / 3DPut fit guides*.)94- **Snap-fit / living hinge / cantilever:** keep peak bending strain under the material's allowable single-assembly strain — **ABS ~6–7%, Nylon-6 ~8.8%, brittle PET ~2%** (*BASF Snap-Fit Manual*); design to ≤50% of yield strain for amorphous (PC/ABS), ≤70% for semi-crystalline (nylon). **These are MOLDED-plastic values — derate hard for FDM** (layer adhesion, notch at the root, raster voids cut the real allowable). **PLA is brittle — treat it PET-class (~1–2%), not nylon-class** (extrapolated — prototype it). A PLA arm sized for nylon's strain snaps off; living hinges want PP/nylon, never PLA.95- **Anisotropy (the orientation check):** FDM inter-layer (Z) tensile strength is only **~50% of in-plane** (real spread 30–75%, material/param-dependent — *RapidMade / FDM interlayer studies*). **Don't load inter-layer bonds in tension** — orient so tensile/bending load runs **along** layers / through solid filament, not pulling layers apart. A boss, snap arm, or screw lug loaded across the layer lines fails at a fraction of its modelled strength.9697Rough properties that set the numbers above (datasheet/molded reference — printed values run lower and print-settings/orientation often dominate, so use the *filament* TDS): **PLA** ~3.5 GPa / ~50 MPa, *brittle*; **PETG** ~2.2 / ~53, ductile; **ABS** ~2.0 / ~41, ductile; **Nylon** ~3.3 / ~86 (*that's dry molded PA66 — printed PA6/PA12 is lower and hygroscopic*), ductile; **CF-nylon** stiff but strongly anisotropic (XY bending modulus ~10 GPa vs Z ~3 GPa, product-specific) (*Prusa / Bambu TDS / superjamie filament wiki*). A fit "verified" against an unstated material proves nothing — the self-confirming-verification trap, one layer down.9899## Geometry gotchas (each hit and solved)100101- **Winding sets extrude direction.** `extrude(sketch, +h)` follows the face normal; a **clockwise**-wound `Polygon` normals **−Z** → extrudes *downward*, and a CW cut profile silently **misses** the boolean (symptom: volume barely drops). `Rectangle`/`Trapezoid` helpers are CCW (safe); hand-built `Polygon([...])` is the trap — list points **CCW**. Assert the volume drop.102- **OCC 3D offset dies on a `scale()`d sphere** (`Standard_Failure: BRep_API: command not done`, degenerate pole/seam). Build domes as a **loft of stacked ellipse sections** instead — also yields a vertical equator tangent that blends into a straight wall.103- **Uniform-wall open cap:** `offset(solid,+t) − solid`, then **`split` flat at the rim plane** → sharp planar edges → fillet **inner only**. Avoid `Kind.ARC`/`Kind.INTERSECTION` on curved offsets when an outer edge must stay sharp.104- **Fillet radius ≥ wall thickness consumes the whole wall** — cap it (e.g. 0.5 mm on a 1 mm wall) and flag the judgment call.105- **Don't blanket-mirror features.** A Y-mirror of an X-symmetric profile looks like a 180° rotation — flips chamfer/keying. Mirror only features referenced to the *moved* edge; edge-referenced features ("5 mm from wide edge") are flip-invariant and must NOT be mirrored (mirroring = regression).106- **Vertical truncation cut:** to amputate a region of a tilted part cleanly, subtract a world-z half-space box, not the irregular feature boundary.107108## Anchor first — solve load-bearing geometry before details109110For "what angle does it rest at": find the natural tilt by **minimizing CG height vs lean angle** — rotate the mesh through θ, compute `cg_z − min_vertex_z` (CG height after dropping to ground), take the local min. Don't hand-derive contacts. **Then ENFORCE that pose** by building the seat at it — the part fixes the angle, robust to unknown CG/weight (weight only affects the stability margin).111112## Cradle retention math (snap-capture a cylinder)113114A trough retains a cylinder only if **opening width < cylinder diameter**:115`opening = 2·(r + clearance)·cos(wrap/2 − 90)` must be `< 2r`.116- **`clearance`** = the print gap you cut the trough oversize by, in mm, **positive** (typical FDM 0.2–0.4 mm so the rod actually inserts; 0 only for a theoretical check). A bigger clearance *widens* the opening → harder to retain, so size wrap against the clearance you'll actually print.117- **Angle convention: degrees.** The `90` is degrees and `wrap` is in degrees. In code, `math.cos` takes **radians** — convert: `math.cos(math.radians(wrap/2 - 90))`. A literal `math.cos(wrap/2 - 90)` is silently wrong.118- Needs **wrap > 180°** by a real margin. 190° ≈ 99.6% of dia = **no retention** (lifts straight out). **~220° ≈ 1 mm constriction, grips ~67% of height** = real snap-fit (press in with slight wall flex).119- "Grip >50% of height" = lips past the equator; height gripped `= r·(1 + sin(wrap/2 − 90))`.120121## Printability metric (FDM, support-free)122123Per face: if it faces **down** (`normal_z < 0`) AND is above the bed, surface incline from horizontal `= arccos(|normal_z|)`; **needs support if < ~45°**. **CRITICAL: exclude bed-contact faces (`centroid_z < ~0.6 mm`)** or the flat bottom false-flags as a 0°-overhang ceiling (it's the first layer). Design tricks: print top-plate-down for caps; a **0.2 mm membrane** closing a counterbore floor lets the printer *bridge* a flat instead of an overhang ledge (drill through after).124125## Warp-awareness (FDM — the big flat base that curls off the plate)126127Warping is a printability failure the **CAD model** controls, the same way support-free is — a slicer brim only fights what the geometry invites. A **large contiguous flat bottom** accumulates differential-shrinkage stress that bows the first layers upward (**sharp corners lift first**, but a rounded slab still bows); on a big part it can lift hard enough to **peel the whole print off a magnetic plate mid-job** (a taco'd base, not a cosmetic blemish). Design it out — biggest lever first:128129- **Kill contiguous flat-bottom area — skeletonize a large base.** Warp force scales with the first-layer *contiguous* area, so a solid slab is the worst case. Replace it with a **perimeter rail + cross-ribs + solid pads only under the load points** (walls, bosses, mounts) and **open windows between**. The original part you're copying often already has these cut-outs — keep them, don't "helpfully" fill them in. Cuts warp, plastic, and print time at once.130- **Round the footprint corners, chamfer the bottom edge.** Corners are the stress concentrators and lift first; a fillet (R ≥ ~8 mm) on the outline plus a small bottom-edge chamfer removes most corner-curl.131- **Design-in mouse-ears / brim tabs** at the corners (thin snap-off discs) instead of leaning on the slicer's brim — the anchor is then part of the model and survives a re-slice / a different operator.132- **Split an oversized flat part.** Two smaller plates warp far less than one big one — corner-lift grows fast with span (a free differential-shrinkage strip bows as span², an upper bound; bed adhesion softens the real curve but the direction holds), so halving the span helps super-linearly. Bolt or dowel them. A **~200 mm solid footprint is already a warp risk** worth splitting or skeletonising.133- **Material is a multiplier, not the cause.** ABS/PETG/CF/PA warp several× more than PLA; a big flat base in a warpy filament is a red flag — surface it to the operator and lean harder on the levers above.134135**Falsifiable check (fold into the verify loop) — gate on contiguity FIRST; corners aggravate, they aren't required.** From the *as-built* mesh (never the nominal params): take bed-contact faces (`fc[:,2] < ~0.6` **and** `fn[:,2] < -0.5`), sum their `contact_area`, and take the footprint bbox of those triangles. Flag when the footprint is **large** (max side > ~150 mm) **and contiguous** (`contact_area / (fx*fy) > ~0.6` — a solid slab ≈ 1.0; a skeletonised rail-and-ribs base drops well below). **Don't require sharp corners** — rounding fixes corner-curl, not the global bow, so a large *rounded* slab must still flag (battle-tested: an R8-filleted 235 mm slab clears any corner test yet is still `contiguity ≈ 1.0` → warps). Score corner-sharpness only as a *severity* aggravator, via a **morphological opening** of the footprint outline (`poly.buffer(-R).buffer(R)`, R≈8; the removed area is the sharp-corner material) — apply it *after* the contiguity gate, since a raw opening also eats thin ribs. Recommend skeletonise + fillet *before the part ever prints*; don't wait for the operator's photo of a base that peeled the plate. *(The ~150 mm / R8 / 0.6 numbers are one-incident + first-principles — they discriminate a slab from its fixes in a synthetic sweep, but pin them against real prints before trusting the exact values.)*136137## Reverse-engineer geometry from photos (no calipers)138139- **Credit card = free scale ruler.** ISO/IEC 7810 ID-1 = 85.60×53.98 mm; threshold its color + `scipy.ndimage.label` for px→mm (check X-vs-Y anisotropy = perspective).140- **Known hole pattern = free homography anchor.** A Pi-stackable board uses **58×49 mm, Ø2.7 (M2.5), 3.5 mm inset**; those 4 holes give an exact 4-point DLT homography (numpy) → rectify to mm, read outline + connector X-positions. **Classical CV (scipy blobs + DLT) beats YOLO** here — deterministic, sub-mm, no training set.141- **What a photo CANNOT give:** connector **heights** above the PCB → look up by part type (KF301 ~10, DC barrel ~11, JST XH ~6 / PH ~5, Type-C ~3.2, 2.54 header ~8.5 mm) or caliper on arrival. This is why a **SAFE variant** (walls open to rim, height-tolerant) is worth carrying alongside the tight BEAUTIFUL one.142- **1:1 PDF for physical overlay:** `figsize=(w_mm/25.4, h_mm/25.4)`, `ax=fig.add_axes([0,0,1,1])`, data limits = part extent, `aspect equal`, **NO `bbox_inches='tight'`** (it rescales!). Add a 10 mm verify-bar; user prints at **100% (no fit-to-page)**, overlays the real part, reports per-axis residuals (screen overlay gives **per-axis**, not uniform, scale error — correct each axis independently; handedness/flip is the #1 confusion).143144## Workflow patterns145146- **Parametric, one source.** Knobs as constants at the top; a dimensional variant is a one-line change or a `VARIANTS` list, **not a forked file**.147- **Emit BOTH variants, never overwrite one file** — `VARIANTS=[("cap_dev",False),("cap_sleek",True)]` writes both `.step/.stl`. Overwriting a single `cap.stl` lost a good cap once.148- Driving Bambu Studio to slice: see memory `bambu-studio-computer-use` (native file dialogs are invisible to screenshots → `open -a BambuStudio file.stl`).149150## Common mistakes151152| Mistake | Fix |153|---|---|154| Defaulting to a 3D solid without checking if it's a plate problem | Step 0 triage; flat plate for in-plane loads, **double-shear + bushing** for loaded pivots (not a single-shear washer-lap), `export_dxf` for sheet |155| "It renders solid" as proof | 3D plots have no occlusion — section + a number |156| A metric computed from the input parameter / a guessed dimension | Self-confirming, not falsifying — probe the as-built solid (`assert bounding_box().max ≈ expected`); state guessed interfaces to the operator |157| Verifying a feature's geometry but not whether it can EXIST in its material (self-tap at the tap-drill Ø, ~1 mm press-fit, PLA snap arm, load across layer lines) | Material-realizability pass — pilot ≈ pitch-Ø not tap-drill; press-fit ≤0.2 mm; strain under the material's allowable; don't load inter-layer (Z) in tension |158| Borrowing another project's venv, or globbing `~/*/venv` for one | Use `~/.venvs/cad/bin/python` (create per Toolchain §). A project-owned venv rotates and dies with its project — cap-model/venv was deleted by a cleanup sweep, and the whole CAD estate once hung off a finished IoT project's brew-python venv |159| `shape.export_stl(...)` | Exports are free functions: `export_stl(shape, path)` |160| CW-wound `Polygon` cut silently misses | List points CCW; assert volume drop per cut |161| `offset()` on a `scale()`d sphere crashes | Loft stacked ellipse sections for domes |162| Blanket-mirroring all features | Mirror only moved-edge-referenced ones |163| Flat print-bed face flagged as overhang | Exclude `centroid_z < ~0.6 mm` from the metric |164| Trusting a photo for connector heights | Photo gives X/Y only; heights from part-type lookup → SAFE variant |165| Shipping a large **solid flat base** (big *contiguous* first-layer footprint; sharp corners worsen it but aren't required) → warps/peels off the plate | Gate on contiguity first (`contact_area/bbox > ~0.6` on a >~150 mm footprint) — skeletonise (perimeter rail + ribs + pads, open windows), fillet corners + chamfer bottom edge, design-in mouse-ears; split if huge; warpy material = red flag (see Warp-awareness) |166| Verifying each part in isolation, never the assembly (parts placed by the hole under test) | Place by an independent datum; assert mating-hole coaxiality + axial stack-up + one-clamp-per-screw-pivot; rotatable/section render, not a self-placed static plot |167| Self-tapping/nutting both rotating members of one pivot, or two crossing bars sharing a plane | One clamp path per screw-pivot (a shoulder/bushing bears if the pin is multiply fixed); crossing bars on different planes or with relief, swept with real thickness |168169## Worked examples170171Core builds, one per technique this skill leans on:172173- **[agentic-cad-fan-stand](https://github.com/evnchn-agentic/agentic-cad-fan-stand)** — cradle retention and rest-angle anchoring, every constraint checked as a number174- **[gimbal-base-cover](https://github.com/evnchn-agentic/gimbal-base-cover)** — support-free printability: teardrop holes, 45° buttressed bosses175- **[connector-cap](https://github.com/evnchn-agentic/connector-cap)** — reverse-engineering a part from photos when the unit ships no CAD176- **[f95d-21700-charger](https://github.com/evnchn-agentic/f95d-21700-charger)** — the verify loop turned on *someone else's* file: a fan housing cut down to a charger, with the electronics bay proven identical to the donor177178Other examples in the [evnchn-agentic](https://github.com/evnchn-agentic) org if you need a different angle.