Signed-off-by: Arnaud Morin <arnaud.gfpv@mailops.fr>
pirouette
Parametric CadQuery model of a 3" FPV race quad frame. It is an interlocking space frame: the parts key into each other with tenons and mortises, so the joints hold it square and there is no jig to build and no bolted-together stack of spacers.
Everything is generated from source — change a number, rerun, get new cut files. Nothing is drawn by hand in a GUI.
./build.sh
That is the whole workflow. It writes build/.
Requirements
-
uv — that's it.
build.shcreates the virtualenv frompyproject.tomlon the first run and reuses it afterwards.curl -LsSf https://astral.sh/uv/install.sh | sh -
Python 3.11 or newer, which uv will fetch for you if the system one is older.
The first run downloads about 1.7 GB. That is not a mistake: CadQuery depends
on OpenCascade and pulls VTK and trame in with it, whether or not you ever open
a viewer. After that a full build takes roughly 50 seconds, most of it the
PNG renders — ./build.sh --no-png cuts it to under 30.
uv.lock pins every version, so a build here is the build you get.
What you get
| Directory | Files | What it's for |
|---|---|---|
build/dxf/ |
4 | flat patterns, one per carbon part — feed these to the router |
build/stl/ |
7 | meshes for the slicer (the printed parts) or for a quick look |
build/step/ |
7 | solid models, if you want to take a part into other CAD |
build/png/ |
4 | assembly renders: iso, top, front, and a grid of all three |
build/ is generated and gitignored. Delete it freely.
Only four parts get a DXF. The other three are printed, so a flat pattern would mean nothing for them.
The DXF is the exact part outline, with no kerf compensation. Offset it by half your tool diameter in CAM, or every part comes out undersize by a whole cutter width.
Bill of materials
Straight from the last build — build.sh prints this table every time, so
trust the run over this copy of it.
| Part | Qty | Stock | Material | g each | g total |
|---|---|---|---|---|---|
| plate | 2 | 2 mm | carbon fibre | 3.86 | 7.72 |
| arm | 4 | 2 mm | carbon fibre | 0.75 | 3.02 |
| motor_base | 8 | 2 mm | carbon fibre | 0.87 | 6.97 |
| spar | 4 | 2 mm | carbon fibre | 1.19 | 4.76 |
| camera_mount | 1 | printed | TPU | 7.87 | 7.87 |
| standoff | 4 | Ø3.5 mm | aluminium | 0.39 | 1.56 |
| lollipop | 1 | printed | TPU | 1.15 | 1.15 |
| frame | 24 | 33.05 |
Carbon at 1.55 g/cm³, TPU at 1.21, aluminium at 2.70.
Not counted, and not modelled: screws, motors, props, camera, VTX, receiver, flight controller. The standoffs are the only hardware in the model.
Hardware you supply
- 4 × M2 aluminium standoff, 3.5 mm across, 15 mm long — that length is the plate gap, so the standoffs are what sets it. Different standoffs mean a different frame; see the levers below.
- M2 screws for those standoffs, and for the flight controller.
- The plates carry both a 20×20 and a 25.5×25.5 mounting pattern, drilled 2.2 mm for M2.
It is built around
- 3" props — 78 mm, and neighbouring discs just touch on this true-X layout. The renders draw them as rings so you can see that.
- A Foxeer Predator 5 Nano camera, which the camera mount is cut for.
- A micro lollipop antenna, held at 45° by
lollipop.py, which clips over the rear standoff pair.
Making the parts
Carbon — the four DXFs cut from 2 mm plate. Every internal corner at a joint already carries a T-bone relief, so a round cutter can actually make the corner; don't "clean up" those little circles, the mating part needs that room.
TPU — camera_mount.stl and lollipop.stl. The STLs are exported at 0.01 mm
chordal deviation, far finer than an FDM printer resolves. Rerun the export at a
tighter tolerance if you are going to resin.
Assembly — dry fit before anything else. Horizontal parts (plates, motor bases) carry the mortises, vertical parts (arms, spars) carry the tenons, and every tenon should land flush with the far face it passes through. Proud jams the joint; short leaves it unseated. The four spars half-lap their neighbours at each corner and are handed — two notches up, two down, alternating round the ring — so check the renders before you glue.
Changing it
Shared dimensions live in frame_params.py, and every part imports them. The
usual things to reach for:
| You want | Change |
|---|---|
| a different prop size | the motor-to-motor gap |
| different carbon stock | the shared thickness |
| a shallower arm | the plate gap — costs stack room |
| looser or tighter joints | the shared fit allowance |
| a taller stack | the plate gap, and buy standoffs to match |
Rerun ./build.sh and the cut files follow. Read AGENT.md first if you are
changing geometry rather than numbers — it records the invariants and the
CadQuery traps that have already cost time here.
Working on a single part
Each part module builds and checks itself, so you can run one on its own without the assembly:
uv run python arm.py # asserts it is one solid, then reports mass
uv run python frame.py # assembly, BOM, exports, renders
They are also CQ-editor scripts: open any of them and it shows the part.
frame.py shows the whole assembly.
Licence
Apache License 2.0. Use it, change it, build frames from it, sell those frames. The conditions are light: keep the copyright notice, note in any file you modify that you changed it, and pass on a copy of the licence.
Two things worth knowing about the fit between this licence and a physical part:
- It covers the files, not the shape. The Python, the comments and the exported drawings are copyrighted work and the licence governs them. The outline of a cut piece of carbon generally is not — functional shapes aren't protected by copyright. Someone who buys a frame, measures it and redraws it in their own CAD is outside this licence entirely. That is normal for open hardware, and not something a different licence would fix.
- It includes a patent grant. Apache-2.0 §3 gives every user a patent licence covering the contributions in here, and revokes it from anyone who sues over the design. This is the main thing it adds over MIT.
No warranty of any kind — see §7. Worth taking literally on a frame: nothing here has been flown, and the design is deliberately built close to its material limits. Check the parts before you trust them at 100 km/h.