Introducing the pirouette FPV frame for GFPV
Signed-off-by: Arnaud Morin <arnaud.gfpv@mailops.fr>
This commit is contained in:
195
AGENT.md
Normal file
195
AGENT.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user