# @author Arnaud Morin # 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)]