# AGENT.md Parametric CadQuery model of a 3" FPV race quad frame. Read before changing anything. Avoid naming specific parameters or functions here — they get renamed as the model evolves. Grep the source for today's names. --- ## Environment venv: `.venv` --- ## The frame An **interlocking space frame** — only the joints hold it square. | Part | qty | orientation | joints | |---|---|---|---| | plate | 2 | horizontal, top and bottom | corner mortises take the arm root tabs | | arm | 4 | vertical, on the diagonals | root tabs through both plates; tip tenon into the upper motor base only | | motor base | 8 | horizontal, one pair per motor | 3 mortises each — 1 arm + 2 spars | | spar | 4 | vertical, outer square | pads through both bases at each end; half-laps its neighbour at each corner | 18 bodies, 4 distinct flat patterns. Horizontal parts carry mortises, vertical parts carry tenons, and every tenon lands **flush** with the far face it passes through — proud jams the joint, short leaves it unseated. --- ## Non-negotiable invariants Break any of these and the model is wrong even if it builds. 1. One file is the single source of truth for every shared dimension. Frame parts import it, never redefine locally. 2. Each kind of part is ONE solid, placed by a transform — never bespoke copies. Verify: residual between any two placed copies must be zero. 3. Assertions go on the built solid, not the parameter. 4. The shared parameter file no longer self-checks. --- ## Design rules learned the hard way - **T-bone relief vs. plain fillet.** A round endmill can't cut a sharp internal corner. A corner that mates with nothing gets a plain fillet; a corner at a joint needs a T-bone (a pocket cut into the shoulder) instead, or the fillet leaves stray material where the mating part needs to sit. - **The spar's symmetry is forced.** Its two lap notches sit on the same edge, so closing all four corners needs alternating flips, not just rotation. - **The prop sizes the frame; hardware sizes the plate.** Two independent scales — don't tie the plate's scale to frame size, or it drives a mortise into a bolt hole. --- ## CadQuery traps that have already bitten Each of these cost real time here. None is obvious from the error. - **Never leave a bare `cq.Color` at module scope** in a file CQ-editor loads. It walks the module's globals and compares them to the object being shown; `Color.__eq__` does `self.toTuple() == other.toTuple()`, which explodes on an Assembly. Keep colours inside a dict — that is why the part colours never caused it. Symptom: `AttributeError: toTuple is not an attribute of `, from a line you did not write. - **A fillet can succeed and hand back an invalid solid.** No exception, `isValid()` False, and the badness only surfaces in a later boolean. Check `isValid()` after filleting anything sculpted. - **Adjacent fillets compete for the gap between them.** Two radii need their tangent lengths — `r / tan(angle / 2)` each — to fit the distance between the corners. Moving a *different* point can change an angle just enough to break a fillet that worked, so when one fails, measure the neighbouring gaps rather than chasing the point you last edited. - **Loft sections must have the same vertex count**, or it fails outright with `StdFail_NotDone`. Add midpoints to the simpler wire. - **A warped ruled face cannot be filleted at any radius.** If a loft's two edges are not parallel the surface between them is a hyperbolic paraboloid. Subdividing both wires splits it into near-planar strips, gives the identical solid, and then it fillets. - **Mirror the solid, not the wires,** when building a cutter from a quadrant. `mirrorX().mirrorY()` on a wire gives the right volume with coincident faces and an invalid shape, and cutting with an invalid tool leaves the part invalid too. - **A workplane made from a face has its normal pointing out of the part**, so `cutBlind` needs a negative distance. A positive one sits in fresh air and removes nothing, silently. - **`BoundingBox()` over-reports on trimmed spline faces** — even `AddOptimal`. It made a symmetric part look lopsided. Slice the solid or use the tessellated vertices when the number matters. --- ## The Z stack Bottom to top: bottom plate, a gap for the stack hardware, top plate. The two motor bases float in Z, symmetric about the arm's mid-plane, pinned to neither plate. The arm tenons into the upper base only; it just tapers clear of the lower one. Derive dependent dimensions (arm height, spar body height) from the stack — don't hand-patch them. --- ## Workflow that actually catches things ``` .venv/bin/python3 plate.py # each part checks it is one solid, .venv/bin/python3 arm.py # then reports its mass .venv/bin/python3 motor_base.py .venv/bin/python3 spar.py .venv/bin/python3 camera_mount.py # printed accessory, not a frame body .venv/bin/python3 standoff.py # turned hardware, in the assembly .venv/bin/python3 frame.py # assembly, BOM, exports, renders ``` The shared parameter file has no self-check and prints nothing — running it on its own proves nothing. `./build.sh` runs exactly that list, after a `uv sync --inexact` that creates `.venv` from `pyproject.toml` if it is missing and leaves any extras you keep in there alone. - `render.py` is shared infrastructure — use it, don't rewrite it. Only the assembly renders; the part modules deliberately do not. - Look at the assembly PNG. "It builds" is not a result, and a part module passing only tells you it came out as one solid. - On a clash, localise it: intersect the two solids, print the bounding box. - Scratch scripts go in the job's tmp directory or `build/` — never the repo root. --- ## Conventions - One module per part at the repo root. Every one has the same shape, in this order — the check sits immediately after the result and OUTSIDE `__main__`, so it fires on import too, which is how the assembly gets it: ```python result = () assert result.solids().size() == 1, " is not one solid" if "show_object" not in globals(): # running outside CQ-editor def show_object(*args, **kwargs): pass show_object(result) if __name__ == "__main__": ... # mass report only ``` - **One solid is the only check a part carries.** Don't add more to a part module; anything about how parts fit belongs to the assembly. - Millimetres. Comment sparingly. Don't overthink — keep shapes simple. - `build/` is generated and gitignored, and only the assembly writes it: `step/` and `stl/` one per part, `dxf/` one per flat pattern, `png/` the assembly views. --- ## Known tensions This frame is built close to its material limits on purpose. **Don't "fix" a thin wall just because it looks thin** — check for a tighter self-check gate on it first; that's a sign the tradeoff was deliberate. Trust the live report over any number written here. If a joint-wall gate fails, the fix is almost never the gate — it's whichever upstream choice is squeezing it. --- ## Levers, if asked to change something | Ask | Where to look | |---|---| | different prop | the prop diameter input | | different stock thickness | the shared thickness parameter | | less steep arm | reduce the plate gap (costs stack room) | | looser / tighter joints | the shared fit allowance | | thicker / thinner spar body | the spar body height input | | arm engages the lower base again | mirror the tip notch/block/T-bone to the bottom, add the matching joint check | --- ## Orchestration note Fanning out to multiple agents: lock the shared interface spec first, in one place. Part builders should not edit the shared parameter file concurrently — have them define anything missing locally and report it for promotion.