Introducing the pirouette FPV frame for GFPV

Signed-off-by: Arnaud Morin <arnaud.gfpv@mailops.fr>
This commit is contained in:
Arnaud Morin
2026-09-03 23:39:06 +02:00
commit 0b96c6bcfe
17 changed files with 3948 additions and 0 deletions

173
plate.py Normal file
View File

@@ -0,0 +1,173 @@
# @author Arnaud Morin <arnaud.gfpv@mailops.fr>
# SPDX-License-Identifier: Apache-2.0
#
from math import cos, radians, sin
import cadquery as cq
from frame_params import (CF_DENSITY, FIT, TBONE_D, TENON_L, THICKNESS)
def _custom_filets(wp, corners, tol=1e-4):
"""The edges that run through the flat pattern (parallel to Z) at the
given (x, y) corners."""
picked = []
for e in wp.edges().vals():
vs = e.Vertices()
if len(vs) != 2:
continue
a, b = vs[0].toTuple(), vs[1].toTuple()
if abs(a[0] - b[0]) > 1e-6 or abs(a[1] - b[1]) > 1e-6:
continue
if any(abs(a[0] - cx) < tol and abs(a[1] - cy) < tol for cx, cy in corners):
picked.append(e)
return wp.newObject(picked)
def _mortises():
"""(x, y, angle) of the four arm root mortises, in the plate's own frame.
angle is the mortise's long axis, which is radial."""
return [(25.3 * cos(radians(a)),
25.3 * sin(radians(a)), a) for a in (45.0, 135.0, 225.0, 315.0)]
def plate():
# one quadrant, from the +X axis to the +Y axis
quadrant = [
(21, 0.0),
(15, 7.0),
(28.5, 22.9),
(13.9, 23.7),
(7.5, 20.5),
(0.0, 20.5),
]
# mirrored across the Y axis (x -> -x) gives quadrant 2, joining quadrant
# 1 into the top half -- both ends now sit on the X axis, so mirrorX()
# below can close the loop by mirroring that half across it in turn
top_half = quadrant + [(-x, y) for x, y in reversed(quadrant)][1:]
p = (
cq.Workplane("XY")
.polyline(top_half)
.mirrorX()
.extrude(THICKNESS)
)
# Filets
p = _custom_filets(p, [(21, 0), (-21, 0), (-21, -0), (21, -0)]).fillet(2)
p = _custom_filets(p, [(15, 7), (-15, 7), (-15, -7), (15, -7)]).fillet(2)
p = _custom_filets(p, [(28.5, 22.9), (-28.5, 22.9), (-28.5, -22.9), (28.5, -22.9)]).fillet(4.5)
p = _custom_filets(p, [(13.9, 23.7), (-13.9, 23.7), (-13.9, -23.7), (13.9, -23.7)]).fillet(2)
p = _custom_filets(p, [(7.5, 20.5), (-7.5, 20.5), (-7.5, -20.5), (7.5, -20.5)]).fillet(6.5)
# Add holes for 20x20 FC
p = (
p.faces(">Z")
.workplane()
.pushPoints([(10, 10), (-10, 10), (-10, -10), (10, -10)])
# Holes are 2.2 to fit 2.0 screws
.hole(2.2)
)
# Add holes for 25.5x25.5 FC
p = (
p.faces(">Z")
.workplane()
.pushPoints([(18.03, 0), (-18.03, 0), (0, -18.03), (0, 18.03)])
# Holes are 2.2 to fit 2.0 screws
.hole(2.2)
)
# Add holes for standoff
x, y = (14, 20)
p = p.faces(">Z").workplane().pushPoints(
[(sx * x, sy * y) for sx in (-1.0, 1.0) for sy in (-1.0, 1.0)]
).hole(2.2)
# Arm mortises
length, width = TENON_L + FIT, THICKNESS
r = TBONE_D / 2.0
hl, hw = length / 2.0, width / 2.0
body = (cq.Workplane("XY")
.polyline([(-hl, -hw), (hl, -hw), (hl, hw), (-hl, hw)])
.close().extrude(THICKNESS))
bones = cq.Workplane("XY")
for u in (-(hl - r), hl - r):
for v in (-hw, hw):
bones = bones.moveTo(u, v).circle(r)
bones = bones.extrude(THICKNESS)
for x, y, a in _mortises():
# two separate cuts, not one combined cutter -- a compound of the
# rectangle plus 4 tangent circles confuses the boolean into leaving
# sliver solids behind
p = p.cut(body.rotate((0, 0, 0), (0, 0, 1), a).translate((x, y, 0)))
p = p.cut(bones.rotate((0, 0, 0), (0, 0, 1), a).translate((x, y, 0)))
# Triangle cuts to reduce weight
cut1 = (
cq.Workplane("XY")
.polyline([
(6.5, -9),
(6.5, 9),
(1, 0),
]).close()
.extrude(THICKNESS)
.edges("|Z")
.fillet(1.0)
)
cut2 = (
cq.Workplane("XY")
.polyline([
(9.5, -9),
(9.5, 9),
(15, 0),
]).close()
.extrude(THICKNESS)
.edges("|Z")
.fillet(1.0)
)
cut3 = (
cq.Workplane("XY")
.polyline([
(11, 15),
(-11, 15),
(0, 4),
]).close()
.extrude(THICKNESS)
.edges("|Z")
.fillet(1.0)
)
p = (
p.cut(cut1)
.cut(cut1.mirror("YZ"))
)
p = (
p.cut(cut2)
.cut(cut2.mirror("YZ"))
)
p = (
p.cut(cut3)
.cut(cut3.mirror("XZ"))
)
return p
result = plate()
assert result.solids().size() == 1, "plate 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__":
v = result.val().Volume()
print("plate mass %.2f g each, %.2f g for two" % (v * CF_DENSITY, 2 * v * CF_DENSITY))