Files
pirouette/arm.py
Arnaud Morin 0b96c6bcfe Introducing the pirouette FPV frame for GFPV
Signed-off-by: Arnaud Morin <arnaud.gfpv@mailops.fr>
2026-09-03 23:39:06 +02:00

154 lines
4.6 KiB
Python

# @author Arnaud Morin <arnaud.gfpv@mailops.fr>
# SPDX-License-Identifier: Apache-2.0
#
import cadquery as cq
from frame_params import (CF_DENSITY, PLATE_GAP, SPAR_H, TENON_L,
THICKNESS, TBONE_D)
# The arm spans the plate gap, so its height IS PLATE_GAP -- the body
# reaches the plates' inner faces at +-HALF_H and every tenon stands one
# THICKNESS proud of that, landing flush with the far face.
HALF_H = PLATE_GAP / 2.0
# and the lower motor base's top face, which hangs SPAR_H below the upper
# one, i.e. below the top plate
BASE_Z = HALF_H - SPAR_H
def _custom_filets(wp):
"""Every vertical edge of the flat pattern (parallel to Y) -- one per
corner, found automatically instead of being listed by hand."""
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[2] - b[2]) > 1e-6:
continue
picked.append(e)
return wp.newObject(picked)
def arm():
part = cq.Workplane("XZ").polyline([
(0.0, HALF_H),
# first tenon, the one that fit in top plate
(3.8, HALF_H),
(3.8, HALF_H + THICKNESS),
(3.8 + TENON_L, HALF_H + THICKNESS),
(3.8 + TENON_L, HALF_H),
# End of top plate
(12, HALF_H),
(12, HALF_H + THICKNESS),
# Arm to motor base
(20.0, HALF_H + THICKNESS),
# Begining of motor base
(20.0, HALF_H),
(25.0, HALF_H),
# Tenon into motor base
(25, HALF_H + THICKNESS),
(25 + TENON_L, HALF_H + THICKNESS),
# Under the motor base
(25 + TENON_L, HALF_H),
(32, HALF_H),
# Come back to bottom plate
(32, BASE_Z),
# Tenon under the motor base, symmetrical to the one above
(25 + TENON_L, BASE_Z),
(25 + TENON_L, BASE_Z - THICKNESS),
(25, BASE_Z - THICKNESS),
(25, BASE_Z),
(20, BASE_Z),
# Diag
(13, -(HALF_H + THICKNESS)),
(12, -(HALF_H + THICKNESS)),
(12, -HALF_H),
# Tenon in bottom plate
(3.8 + TENON_L, -HALF_H),
(3.8 + TENON_L, -(HALF_H + THICKNESS)),
(3.8, -(HALF_H + THICKNESS)),
(3.8, -HALF_H),
(1.5, -HALF_H)
])
part = (
part.lineTo(2, -7)
.threePointArc(
(5, 0.0),
(0.0, 6)
)
)
# Extrude. Both = True do the extrusion along the Y axis each face,
# so no need to translate later :)
part = part.close().extrude(THICKNESS / 2.0, both=True)
# Hole
cut = (
cq.Workplane('XZ')
.polyline([
(7, 4),
(17, 4),
(12.5, -5),
(6.5, -5),
])
.threePointArc(
(7.5, 0.0),
(6, 4),
)
.close()
.extrude(THICKNESS / 2.0, both=True)
)
# smooth every sharp corner, no bare angles left
cut = _custom_filets(cut).fillet(1.0)
part = part.cut(cut)
# T-Bones on tenons (x, z)
tbones = [
# root tenon into top plate, shifted 1 mm right
(3.8 - TBONE_D / 2 + 0.05, HALF_H),
(3.8 + TENON_L + TBONE_D / 2 - 0.05, HALF_H),
# tenon into motor base
(25 - TBONE_D / 2 + 0.05, HALF_H),
(25 + TENON_L + TBONE_D / 2 - 0.05, HALF_H),
# tenon under the motor base
(25 - TBONE_D / 2 + 0.05, BASE_Z),
(25 + TENON_L + TBONE_D / 2 - 0.05, BASE_Z),
# root tenon into bottom plate, shifted 1 mm right
(3.8 - TBONE_D / 2 + 0.05, -HALF_H),
(3.8 + TENON_L + TBONE_D / 2 - 0.05, -HALF_H),
# 3 more sharp inside corners that need relief, away from any tenon:
# where the flat top steps up into the raised motor-base flange...
(12 - TBONE_D / 2 + 0.05, HALF_H),
# ... its mirror on the bottom edge ...
(12 - TBONE_D / 2 + 0.05, -HALF_H),
# ... and the sharp corner where the diagonal brace meets the tip block
(20 + TBONE_D / 2 - 0.05, HALF_H),
]
# Cut the T-Bones
cut = cq.Workplane("XZ")
for x, z in tbones:
cut = cut.moveTo(x, z).circle(TBONE_D / 2.0)
part = part.cut(cut.extrude(THICKNESS / 2.0, both=True))
return part
result = arm()
assert result.solids().size() == 1, "arm 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__":
vol = result.val().Volume()
m = vol * CF_DENSITY
print(f"arm mass {m:.2f} g each, {4*m:.2f} g for four")