169 lines
7.7 KiB
Python
169 lines
7.7 KiB
Python
# @author Arnaud Morin <arnaud.gfpv@mailops.fr>
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
from math import cos, radians, sin, sqrt
|
|
|
|
# --- material ---------------------------------------------------------------
|
|
|
|
THICKNESS = 2.0
|
|
CF_DENSITY = 1.55e-3 # g/mm3, for mass reporting
|
|
|
|
# --- joint fit --------------------------------------------------------------
|
|
|
|
FIT = 0.2 # a mortise is its tenon's length + FIT (0.1 per
|
|
# side); the width is a snug match, no FIT added
|
|
TBONE_D = 1.0 # T-bone relief circle = one cutter diameter
|
|
TENON_L = 4.0 # every tenon in the frame is this long -- the arm's
|
|
# tip block and the spar's own tenon both key off it
|
|
|
|
# --- overall layout ---------------------------------------------------------
|
|
|
|
SPAR_H = 4.0
|
|
SPAR_OFFSET = 2.5 # spar centreline clears the motor axis by this much,
|
|
# outboard -- feeds R_SPAR_LINE below. motor_base.py
|
|
# and spar.py each hold their own matching copy of
|
|
# this value now, not linked by import any more, so
|
|
# keep them in step by hand
|
|
|
|
# clear gap between the two plates
|
|
PLATE_GAP = 15.0
|
|
|
|
# --- computed layout --------------------------------------------------------
|
|
# On a true X, motor to motor gap is the size of the prop
|
|
MOTOR_GAP = 78.0
|
|
# thank you pythagore
|
|
MOTOR_DIAG = MOTOR_GAP * sqrt(2.0)
|
|
|
|
|
|
MOTOR_ANGLES = (45.0, 135.0, 225.0, 315.0) # true-X, one per corner
|
|
|
|
# The plate's own dimensions below are absolute, not derived from MOTOR_DIAG
|
|
# or anything else that grows with the frame. Sheet thicknesses can't scale
|
|
# -- they're stock -- and the stack patterns and motor bolt square are
|
|
# hardware, so the plate is sized by hardware that doesn't shrink with the
|
|
# frame: the 25.5 stack square, and the arm mortises that have to clear its
|
|
# bolt rim. A plate that scaled down with a 110 mm wheelbase once drove a
|
|
# mortise 0.6 mm off the bolt holes -- these numbers are the smallest that
|
|
# keep that wall, and they stay fixed regardless of frame size.
|
|
|
|
R_SPAR_LINE = MOTOR_GAP / 2.0 + SPAR_OFFSET # |x| or |y| of a spar centreline
|
|
|
|
# --- Z stack-up -------------------------------------------------------------
|
|
# z = 0 is the underside of the bottom plate; everything below is measured
|
|
# up from there.
|
|
|
|
Z_TOP = THICKNESS + PLATE_GAP # underside of the top plate
|
|
Z_TOP_FACE = Z_TOP + THICKNESS
|
|
ARM_HEIGHT = Z_TOP_FACE # 19.0, the arm's own full height
|
|
Z_ARM_MID = ARM_HEIGHT / 2.0 # the arm's mid-plane
|
|
|
|
# the upper motor base is pinned flush with the top plate: its top face
|
|
# sits exactly level with the top plate's top face, since every part
|
|
# shares one THICKNESS -- the motor sits at the same level as the top
|
|
# plate rather than buried lower in the stack
|
|
Z_MOTOR_BASE = Z_TOP # underside of the upper base
|
|
|
|
# The lower base hangs SPAR_H below the upper one. SPAR_H is still
|
|
# the input that sets the spar's clear span; it just no longer places the
|
|
# lower base symmetrically about the arm's mid-plane -- with the upper base
|
|
# now pinned high, the lower one floats further from the bottom plate than
|
|
# it used to, held by the spar (the arm doesn't reach it at all).
|
|
Z_MOTOR_BASE_BOT_FACE = Z_MOTOR_BASE - SPAR_H # top face of the lower base
|
|
Z_MOTOR_BASE_BOT = Z_MOTOR_BASE_BOT_FACE - THICKNESS
|
|
Z_SPAR_MID = Z_MOTOR_BASE - SPAR_H / 2.0 # body top = base underside
|
|
|
|
# --- plate ------------------------------------------------------------------
|
|
# The plate's own outline, web relief, hole layout and corner-lobe geometry
|
|
# are all plate.py's own baked-literal business now -- none of it is a
|
|
# cross-part fact any more, so it does not live here. The one thing that
|
|
# does: where the arm's root tenon meets the plate, because arm_placements()
|
|
# below has to put the arm's local origin at the radius that lines up with
|
|
# it.
|
|
R_PLATE_MORTISE = 25.3 # arm root mortise centre, on the diagonal --
|
|
# matches plate.py's own hardcoded mortise
|
|
# radius; the two aren't linked by import any
|
|
# more, so keep them in step by hand
|
|
|
|
# --- arm ----------------------------------------------------------------
|
|
# The arm's own outline lives in arm.py now, as hardcoded literals. What
|
|
# stays here is only what places the arm in the assembly.
|
|
|
|
ARM_ROOT_TAB_X = 5.8 # tab centre -- arm.py's root tenon is 1 mm
|
|
# out from the root end, so this and
|
|
# R_PLATE_MORTISE both moved 1 mm to match
|
|
ARM_R_ROOT = R_PLATE_MORTISE - ARM_ROOT_TAB_X # radius of the arm's root
|
|
# end face
|
|
|
|
|
|
# --- placement helpers ------------------------------------------------------
|
|
|
|
def motor_positions():
|
|
"""The four motor axes, true-X."""
|
|
return [(MOTOR_DIAG/2 * cos(radians(a)), MOTOR_DIAG/2 * sin(radians(a)))
|
|
for a in MOTOR_ANGLES]
|
|
|
|
|
|
def plate_placements():
|
|
"""(x, y, z, rot_z) for the bottom and the top plate -- one part, twice."""
|
|
return [(0.0, 0.0, 0.0, 0.0), (0.0, 0.0, Z_TOP, 0.0)]
|
|
|
|
|
|
def arm_placements():
|
|
"""(x, y, z, rot_z). Rotate about world Z, then translate. The local
|
|
origin is the root end face, on the arm's mid-plane."""
|
|
return [(ARM_R_ROOT * cos(radians(a)), ARM_R_ROOT * sin(radians(a)),
|
|
Z_ARM_MID, a) for a in MOTOR_ANGLES]
|
|
|
|
|
|
def motor_placements():
|
|
"""(x, y, z, rot_z, flip) -- EIGHT bases: one above and one below every
|
|
corner, all the same part. rot_z puts local +Y radially outward.
|
|
|
|
The lower four are turned over. _loc flips about the part's own X axis,
|
|
which would swing the arm mortise from -Y to +Y, so the flip is paired with
|
|
an extra 180 deg of rot_z: together they mirror the part about its own Y
|
|
axis instead, which leaves the arm mortise on -Y, swaps the two spar
|
|
mortises for each other, and maps the outline onto itself (it is symmetric
|
|
in x). After the X flip the part hangs in local -T..0, hence the +T on z.
|
|
"""
|
|
out = []
|
|
for (x, y), a in zip(motor_positions(), MOTOR_ANGLES):
|
|
out.append((x, y, Z_MOTOR_BASE, a - 90.0, 0.0))
|
|
for (x, y), a in zip(motor_positions(), MOTOR_ANGLES):
|
|
out.append((x, y, Z_MOTOR_BASE_BOT + THICKNESS, a + 90.0, 180.0))
|
|
return out
|
|
|
|
|
|
def spar_placements():
|
|
"""(x, y, z, rot_z, flip) -- one per edge of the outer square.
|
|
|
|
flip is 0 or 180 deg about the part's OWN length axis, applied BEFORE
|
|
rot_z. Both lap notches of the part are on its +Z edge, so the flips must
|
|
ALTERNATE round the ring: every corner then has one notch facing up and its
|
|
mate facing down. (Front/rear flipped together instead leaves two corners
|
|
correct and drives two tongues into each other at the other two.)
|
|
"""
|
|
d = R_SPAR_LINE
|
|
return [
|
|
(0.0, +d, Z_SPAR_MID, 0.0, 0.0), # front, notch up
|
|
(+d, 0.0, Z_SPAR_MID, 90.0, 180.0), # right, notch down
|
|
(0.0, -d, Z_SPAR_MID, 0.0, 0.0), # rear, notch up
|
|
(-d, 0.0, Z_SPAR_MID, 90.0, 180.0), # left, notch down
|
|
]
|
|
|
|
|
|
# --- standoffs --------------------------------------------------------------
|
|
# Four posts holding the plates apart. Their height is PLATE_GAP itself, so
|
|
# they stand on the bottom plate's top face and meet the top plate's underside
|
|
# exactly. The x/y pattern mirrors plate.py's own standoff holes -- not linked
|
|
# by import, so keep the two in step by hand.
|
|
STANDOFF_XY = (14.0, 20.0)
|
|
|
|
|
|
def standoff_placements():
|
|
"""(x, y, z, rot_z) -- one per corner of the standoff pattern."""
|
|
x, y = STANDOFF_XY
|
|
return [(sx * x, sy * y, THICKNESS, 0.0)
|
|
for sx in (-1.0, 1.0) for sy in (-1.0, 1.0)]
|