feat: Torsion resistant shoulder

This commit is contained in:
Leni Aniva 2024-07-24 12:45:38 -07:00
parent a9b3aa8f70
commit b8c6fb51fd
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
2 changed files with 100 additions and 43 deletions

View File

@ -8,7 +8,9 @@ from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.joints import TorsionJoint, HirthJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator from nhf.touhou.houjuu_nue.electronics import (
Flexor, LinearActuator, LINEAR_ACTUATOR_21,
)
import nhf.geometry import nhf.geometry
import nhf.utils import nhf.utils
@ -286,17 +288,18 @@ class ShoulderJoint(Model):
# Generates a child guard which covers up the internals. The lip length is # Generates a child guard which covers up the internals. The lip length is
# relative to the +X surface of the guard. # relative to the +X surface of the guard.
child_guard_ext: float = 30.0 child_guard_ext: float = 20.0
child_guard_width: float = 25.0 child_guard_width: float = 25.0
# guard length measured from axle # guard length measured from axle
child_lip_length: float = 40.0 child_lip_ext: float = 50.0
child_lip_width: float = 20.0 child_lip_width: float = 20.0
child_lip_height: float = 40.0
child_lip_thickness: float = 5.0
child_conn_hole_diam: float = 4.0 child_conn_hole_diam: float = 4.0
# Measured from centre of axle # Measured from centre of axle
child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30]) child_conn_hole_pos: list[float] = field(default_factory=lambda: [-15, -5, 5, 15])
child_core_thickness: float = 3.0 child_core_thickness: float = 3.0
# Rotates the torsion joint to avoid collisions or for some other purpose # Rotates the torsion joint to avoid collisions or for some other purpose
axis_rotate_bot: float = 90 axis_rotate_bot: float = 90
axis_rotate_top: float = 0 axis_rotate_top: float = 0
@ -311,10 +314,16 @@ class ShoulderJoint(Model):
spool_height: float = 5.0 spool_height: float = 5.0
spool_groove_inset: float = 2.0 spool_groove_inset: float = 2.0
flip: bool = False
actuator: LinearActuator = LINEAR_ACTUATOR_21
def __post_init__(self): def __post_init__(self):
assert self.parent_lip_length * 2 < self.height assert self.parent_lip_length * 2 < self.height
assert self.child_guard_ext > self.torsion_joint.radius_rider
assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness
assert self.spool_base_height > self.spool_groove_depth assert self.spool_base_height > self.spool_groove_depth
assert self.child_lip_height < self.height
assert self.draft_length <= self.actuator.stroke_length
@property @property
def radius(self): def radius(self):
@ -327,6 +336,13 @@ class ShoulderJoint(Model):
""" """
return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection) return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection)
@property
def draft_height(self):
"""
Position of the middle of the spool measured from the middle
"""
return self.height / 2 - self.torsion_joint.total_height - self.spool_base_height / 2
def parent_lip_loc(self, left: bool=True) -> Cq.Location: def parent_lip_loc(self, left: bool=True) -> Cq.Location:
""" """
2d location of the arm surface on the parent side, relative to axle 2d location of the arm surface on the parent side, relative to axle
@ -335,10 +351,15 @@ class ShoulderJoint(Model):
sign = 1 if left else -1 sign = 1 if left else -1
loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90) loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90)
return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir
def child_lip_loc(self) -> Cq.Location:
"""
2d location to middle of lip
"""
return Cq.Location.from2d(self.child_lip_ext - self.child_guard_ext, 0, 180)
@property @property
def _max_contraction_angle(self) -> float: def _max_contraction_angle(self) -> float:
return self.angle_max_deflection + self.angle_neutral return 180 - self.angle_max_deflection + self.angle_neutral
def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid:
""" """
@ -346,7 +367,8 @@ class ShoulderJoint(Model):
""" """
aspect = self.child_guard_width / self.parent_arm_width aspect = self.child_guard_width / self.parent_arm_width
theta = math.radians(self._max_contraction_angle) theta = math.radians(self._max_contraction_angle)
theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) #theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect))
theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect)
angle = math.degrees(theta_p) angle = math.degrees(theta_p)
assert 0 <= angle <= 90 assert 0 <= angle <= 90
# outer radius of the cut, overestimated # outer radius of the cut, overestimated
@ -431,10 +453,7 @@ class ShoulderJoint(Model):
@target(name="parent-bot") @target(name="parent-bot")
def parent_bot(self) -> Cq.Assembly: def parent_bot(self) -> Cq.Assembly:
result = ( return self.parent(top=False)
self.parent(top=False)
)
return result
@target(name="parent-top") @target(name="parent-top")
def parent_top(self) -> Cq.Assembly: def parent_top(self) -> Cq.Assembly:
return self.parent(top=True) return self.parent(top=True)
@ -485,18 +504,15 @@ class ShoulderJoint(Model):
""" """
joint = self.torsion_joint joint = self.torsion_joint
assert all(r < self.child_lip_length for r in self.child_conn_hole_pos)
# Half of the height of the bridging cylinder # Half of the height of the bridging cylinder
dh = self.height / 2 - joint.total_height dh = self.height / 2 - joint.total_height
core_start_angle = 30 core_start_angle = 30
core_end_angle1 = 90
core_end_angle2 = 180
radius_core_inner = joint.radius_rider - self.child_core_thickness radius_core_inner = joint.radius_rider - self.child_core_thickness
core_profile1 = ( core_profile1 = (
Cq.Sketch() Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) .arc((0, 0), joint.radius_rider, core_start_angle, self.angle_max_deflection)
.segment((0, 0)) .segment((0, 0))
.close() .close()
.assemble() .assemble()
@ -504,12 +520,28 @@ class ShoulderJoint(Model):
) )
core_profile2 = ( core_profile2 = (
Cq.Sketch() Cq.Sketch()
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) .arc((0, 0), joint.radius_rider, -core_start_angle, -(90 - self.angle_neutral))
.segment((0, 0)) .segment((0, 0))
.close() .close()
.assemble() .assemble()
.circle(radius_core_inner, mode='s') .circle(radius_core_inner, mode='s')
) )
lip_extension = (
Cq.Solid.makeBox(
length=self.child_lip_ext - self.child_guard_ext,
width=self.child_lip_width,
height=self.child_lip_height,
).cut(Cq.Solid.makeBox(
length=self.child_lip_ext - self.child_guard_ext,
width=self.child_lip_width - self.child_lip_thickness * 2,
height=self.child_lip_height,
).located(Cq.Location((0, self.child_lip_thickness, 0))))
.located(Cq.Location((
self.child_guard_ext,
-self.child_lip_width / 2,
-self.child_lip_height / 2,
)))
)
core_guard = ( core_guard = (
Cq.Workplane('XY') Cq.Workplane('XY')
.box( .box(
@ -518,6 +550,14 @@ class ShoulderJoint(Model):
height=self.height, height=self.height,
centered=(False, True, True), centered=(False, True, True),
) )
#.copyWorkplane(Cq.Workplane('XY'))
#.box(
# length=self.child_lip_ext,
# width=self.child_guard_width,
# height=self.child_lip_height,
# combine=True,
# centered=(False, True, True),
#)
.copyWorkplane(Cq.Workplane('XY')) .copyWorkplane(Cq.Workplane('XY'))
.cylinder( .cylinder(
radius=self.radius, radius=self.radius,
@ -527,12 +567,13 @@ class ShoulderJoint(Model):
) )
.copyWorkplane(Cq.Workplane('XY')) .copyWorkplane(Cq.Workplane('XY'))
.box( .box(
length=self.child_guard_ext, length=self.child_lip_ext,
width=self.child_lip_width, width=self.child_lip_width,
height=self.height - self.torsion_joint.total_height * 2, height=self.height - self.torsion_joint.total_height * 2,
combine='cut', combine='cut',
centered=(False, True, True), centered=(False, True, True),
) )
.union(lip_extension)
.cut(self._contraction_cut_geometry(parent=False)) .cut(self._contraction_cut_geometry(parent=False))
) )
core = ( core = (
@ -548,13 +589,17 @@ class ShoulderJoint(Model):
.union(core_guard) .union(core_guard)
) )
assert self.child_lip_width / 2 <= joint.radius_rider assert self.child_lip_width / 2 <= joint.radius_rider
lip_thickness = joint.rider_disk_height sign = 1 if self.flip else -1
lip = box_with_centre_holes( holes = [Hole(x=sign * x) for x in self.child_conn_hole_pos]
length=self.child_lip_length, lip_obj = MountingBox(
length=self.child_lip_height,
width=self.child_lip_width, width=self.child_lip_width,
height=lip_thickness, thickness=self.child_lip_thickness,
hole_loc=self.child_conn_hole_pos, holes=holes,
hole_diam=self.child_conn_hole_diam, hole_diam=self.child_conn_hole_diam,
centred=(True, True),
generate_side_tags=False,
generate_reverse_tags=False,
) )
theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
@ -562,6 +607,7 @@ class ShoulderJoint(Model):
loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral)
spool_dz = self.height / 2 - self.torsion_joint.total_height spool_dz = self.height / 2 - self.torsion_joint.total_height
spool_angle = 180 + self.angle_neutral spool_angle = 180 + self.angle_neutral
loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location()
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(core, name="core", loc=Cq.Location()) .add(core, name="core", loc=Cq.Location())
@ -569,12 +615,10 @@ class ShoulderJoint(Model):
loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta)) loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta))
.add(joint.rider(rider_slot_begin=180), name="rider_bot", .add(joint.rider(rider_slot_begin=180), name="rider_bot",
loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
.add(lip, name="lip_top", .add(lip_obj.generate(), name="lip",
loc=Cq.Location((self.child_guard_ext, 0, dh))) loc=Cq.Location((self.child_lip_ext - self.child_lip_thickness,0,0), (0,1,0), 90))
.add(lip, name="lip_bot",
loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate)
.add(self._spool(), name="spool", .add(self._spool(), name="spool",
loc=Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) loc=loc_spool_flip * Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle))
) )
return result return result

View File

@ -337,6 +337,19 @@ class WingProfile(Model):
flip_y=self.flip, flip_y=self.flip,
generate_reverse_tags=True, generate_reverse_tags=True,
) )
@submodel(name="spacer-s0-shoulder-act")
def spacer_s0_shoulder_act(self) -> MountingBox:
dx = self.shoulder_joint.draft_height
return MountingBox(
holes=[Hole(x=dx), Hole(x=-dx)],
hole_diam=self.shoulder_joint.actuator.back_hole_diam,
length=self.root_height,
width=10.0,
centred=(True, True),
thickness=self.spacer_thickness,
flip_y=self.flip,
generate_reverse_tags=True,
)
def surface_s0(self, top: bool = False) -> Cq.Workplane: def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10
@ -352,6 +365,8 @@ class WingProfile(Model):
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)), self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)),
("shoulder_right", ("shoulder_right",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)),
("shoulder_act",
self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(150, -40)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)),
("electronic_mount", Cq.Location.from2d(-45, 75, 64)), ("electronic_mount", Cq.Location.from2d(-45, 75, 64)),
] ]
@ -393,6 +408,7 @@ class WingProfile(Model):
for o, tag in [ for o, tag in [
(self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"), (self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"),
(self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"), (self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"),
(self.spacer_s0_shoulder_act().generate(), "shoulder_act"),
(self.spacer_s0_base().generate(), "base"), (self.spacer_s0_base().generate(), "base"),
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"), (self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
]: ]:
@ -599,13 +615,11 @@ class WingProfile(Model):
) )
return profile return profile
def surface_s1(self, front: bool = True) -> Cq.Workplane: def surface_s1(self, front: bool = True) -> Cq.Workplane:
shoulder_h = self.shoulder_joint.child_height
h = (self.shoulder_joint.height - shoulder_h) / 2
tags_shoulder = [ tags_shoulder = [
("shoulder_bot", Cq.Location.from2d(0, h, 90)), ("shoulder",
("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), Cq.Location((0, self.shoulder_height / 2, 0)) *
self.shoulder_joint.child_lip_loc()),
] ]
h = self.elbow_height / 2
rot_elbow = Cq.Location.rot2d(self.elbow_rotate) rot_elbow = Cq.Location.rot2d(self.elbow_rotate)
loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc()
tags_elbow = [ tags_elbow = [
@ -622,16 +636,20 @@ class WingProfile(Model):
profile, self.panel_thickness, tags, reverse=front) profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s1-shoulder") @submodel(name="spacer-s1-shoulder")
def spacer_s1_shoulder(self) -> MountingBox: def spacer_s1_shoulder(self) -> MountingBox:
sign = -1 if self.flip else 1
holes = [ holes = [
Hole(x) Hole(x=sign * x)
for x in self.shoulder_joint.child_conn_hole_pos for x in self.shoulder_joint.child_conn_hole_pos
] ]
return MountingBox( return MountingBox(
length=50.0, # FIXME: magic length=self.shoulder_joint.child_lip_height,
width=self.s1_thickness, width=self.s1_thickness,
thickness=self.spacer_thickness, thickness=self.spacer_thickness,
holes=holes, holes=holes,
centred=(True, True),
hole_diam=self.shoulder_joint.child_conn_hole_diam, hole_diam=self.shoulder_joint.child_conn_hole_diam,
centre_left_right_tags=True,
centre_bot_top_tags=True,
) )
@submodel(name="spacer-s1-elbow") @submodel(name="spacer-s1-elbow")
def spacer_s1_elbow(self) -> MountingBox: def spacer_s1_elbow(self) -> MountingBox:
@ -664,8 +682,7 @@ class WingProfile(Model):
param=self.s1_thickness) param=self.s1_thickness)
) )
for o, t in [ for o, t in [
(self.spacer_s1_shoulder(), "shoulder_bot"), (self.spacer_s1_shoulder(), "shoulder"),
(self.spacer_s1_shoulder(), "shoulder_top"),
(self.spacer_s1_elbow(), "elbow_top"), (self.spacer_s1_elbow(), "elbow_top"),
(self.spacer_s1_elbow(), "elbow_bot"), (self.spacer_s1_elbow(), "elbow_bot"),
(self.spacer_s1_elbow_act(), "elbow_act"), (self.spacer_s1_elbow_act(), "elbow_act"),
@ -988,13 +1005,8 @@ class WingProfile(Model):
if "s1" in parts: if "s1" in parts:
result.add(self.assembly_s1(), name="s1") result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts: if "s1" in parts and "shoulder" in parts:
( for i in range(len(self.shoulder_joint.child_conn_hole_pos)):
result result.constrain(f"s1/shoulder?conn{i}", f"shoulder/child/lip?conn{i}", "Plane")
.constrain("s1/shoulder_top?conn0", f"shoulder/child/lip_{tag_top}?conn0", "Plane")
.constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane")
.constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane")
.constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane")
)
if "elbow" in parts: if "elbow" in parts:
angle = self.elbow_joint.motion_span * elbow_wrist_deflection angle = self.elbow_joint.motion_span * elbow_wrist_deflection
result.add(self.elbow_joint.assembly( result.add(self.elbow_joint.assembly(
@ -1327,6 +1339,7 @@ class WingL(WingProfile):
self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0
self.wrist_rotate = -self.wrist_joint.angle_neutral self.wrist_rotate = -self.wrist_joint.angle_neutral
self.wrist_joint.flip = False self.wrist_joint.flip = False
self.shoulder_joint.flip = True
super().__post_init__() super().__post_init__()