196 lines
7.8 KiB
Markdown
196 lines
7.8 KiB
Markdown
# 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
|
|
<Assembly>`, 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 = <part>()
|
|
|
|
assert result.solids().size() == 1, "<part> 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.
|