cosplay: Touhou/Houjuu Nue #4
|
@ -8,7 +8,9 @@ from nhf.parts.springs import TorsionSpring
|
|||
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
|
||||
from nhf.parts.joints import TorsionJoint, HirthJoint
|
||||
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.utils
|
||||
|
||||
|
@ -286,17 +288,18 @@ class ShoulderJoint(Model):
|
|||
|
||||
# Generates a child guard which covers up the internals. The lip length is
|
||||
# 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
|
||||
# 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_height: float = 40.0
|
||||
child_lip_thickness: float = 5.0
|
||||
child_conn_hole_diam: float = 4.0
|
||||
# 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
|
||||
|
||||
|
||||
# Rotates the torsion joint to avoid collisions or for some other purpose
|
||||
axis_rotate_bot: float = 90
|
||||
axis_rotate_top: float = 0
|
||||
|
@ -311,10 +314,16 @@ class ShoulderJoint(Model):
|
|||
spool_height: float = 5.0
|
||||
spool_groove_inset: float = 2.0
|
||||
|
||||
flip: bool = False
|
||||
actuator: LinearActuator = LINEAR_ACTUATOR_21
|
||||
|
||||
def __post_init__(self):
|
||||
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_base_height > self.spool_groove_depth
|
||||
assert self.child_lip_height < self.height
|
||||
assert self.draft_length <= self.actuator.stroke_length
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@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:
|
||||
"""
|
||||
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
|
||||
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
|
||||
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
|
||||
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:
|
||||
"""
|
||||
|
@ -346,7 +367,8 @@ class ShoulderJoint(Model):
|
|||
"""
|
||||
aspect = self.child_guard_width / self.parent_arm_width
|
||||
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)
|
||||
assert 0 <= angle <= 90
|
||||
# outer radius of the cut, overestimated
|
||||
|
@ -431,10 +453,7 @@ class ShoulderJoint(Model):
|
|||
|
||||
@target(name="parent-bot")
|
||||
def parent_bot(self) -> Cq.Assembly:
|
||||
result = (
|
||||
self.parent(top=False)
|
||||
)
|
||||
return result
|
||||
return self.parent(top=False)
|
||||
@target(name="parent-top")
|
||||
def parent_top(self) -> Cq.Assembly:
|
||||
return self.parent(top=True)
|
||||
|
@ -485,18 +504,15 @@ class ShoulderJoint(Model):
|
|||
"""
|
||||
|
||||
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
|
||||
dh = self.height / 2 - joint.total_height
|
||||
core_start_angle = 30
|
||||
core_end_angle1 = 90
|
||||
core_end_angle2 = 180
|
||||
|
||||
radius_core_inner = joint.radius_rider - self.child_core_thickness
|
||||
core_profile1 = (
|
||||
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))
|
||||
.close()
|
||||
.assemble()
|
||||
|
@ -504,12 +520,28 @@ class ShoulderJoint(Model):
|
|||
)
|
||||
core_profile2 = (
|
||||
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))
|
||||
.close()
|
||||
.assemble()
|
||||
.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 = (
|
||||
Cq.Workplane('XY')
|
||||
.box(
|
||||
|
@ -518,6 +550,14 @@ class ShoulderJoint(Model):
|
|||
height=self.height,
|
||||
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'))
|
||||
.cylinder(
|
||||
radius=self.radius,
|
||||
|
@ -527,12 +567,13 @@ class ShoulderJoint(Model):
|
|||
)
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.box(
|
||||
length=self.child_guard_ext,
|
||||
length=self.child_lip_ext,
|
||||
width=self.child_lip_width,
|
||||
height=self.height - self.torsion_joint.total_height * 2,
|
||||
combine='cut',
|
||||
centered=(False, True, True),
|
||||
)
|
||||
.union(lip_extension)
|
||||
.cut(self._contraction_cut_geometry(parent=False))
|
||||
)
|
||||
core = (
|
||||
|
@ -548,13 +589,17 @@ class ShoulderJoint(Model):
|
|||
.union(core_guard)
|
||||
)
|
||||
assert self.child_lip_width / 2 <= joint.radius_rider
|
||||
lip_thickness = joint.rider_disk_height
|
||||
lip = box_with_centre_holes(
|
||||
length=self.child_lip_length,
|
||||
sign = 1 if self.flip else -1
|
||||
holes = [Hole(x=sign * x) for x in self.child_conn_hole_pos]
|
||||
lip_obj = MountingBox(
|
||||
length=self.child_lip_height,
|
||||
width=self.child_lip_width,
|
||||
height=lip_thickness,
|
||||
hole_loc=self.child_conn_hole_pos,
|
||||
thickness=self.child_lip_thickness,
|
||||
holes=holes,
|
||||
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
|
||||
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)
|
||||
spool_dz = self.height / 2 - self.torsion_joint.total_height
|
||||
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 = (
|
||||
Cq.Assembly()
|
||||
.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))
|
||||
.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)
|
||||
.add(lip, name="lip_top",
|
||||
loc=Cq.Location((self.child_guard_ext, 0, dh)))
|
||||
.add(lip, name="lip_bot",
|
||||
loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate)
|
||||
.add(lip_obj.generate(), name="lip",
|
||||
loc=Cq.Location((self.child_lip_ext - self.child_lip_thickness,0,0), (0,1,0), 90))
|
||||
.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
|
||||
|
||||
|
|
|
@ -337,6 +337,19 @@ class WingProfile(Model):
|
|||
flip_y=self.flip,
|
||||
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:
|
||||
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)),
|
||||
("shoulder_right",
|
||||
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)),
|
||||
("electronic_mount", Cq.Location.from2d(-45, 75, 64)),
|
||||
]
|
||||
|
@ -393,6 +408,7 @@ class WingProfile(Model):
|
|||
for o, tag in [
|
||||
(self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"),
|
||||
(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_electronic_mount().generate(), "electronic_mount"),
|
||||
]:
|
||||
|
@ -599,13 +615,11 @@ class WingProfile(Model):
|
|||
)
|
||||
return profile
|
||||
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 = [
|
||||
("shoulder_bot", Cq.Location.from2d(0, h, 90)),
|
||||
("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)),
|
||||
("shoulder",
|
||||
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)
|
||||
loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc()
|
||||
tags_elbow = [
|
||||
|
@ -622,16 +636,20 @@ class WingProfile(Model):
|
|||
profile, self.panel_thickness, tags, reverse=front)
|
||||
@submodel(name="spacer-s1-shoulder")
|
||||
def spacer_s1_shoulder(self) -> MountingBox:
|
||||
sign = -1 if self.flip else 1
|
||||
holes = [
|
||||
Hole(x)
|
||||
Hole(x=sign * x)
|
||||
for x in self.shoulder_joint.child_conn_hole_pos
|
||||
]
|
||||
return MountingBox(
|
||||
length=50.0, # FIXME: magic
|
||||
length=self.shoulder_joint.child_lip_height,
|
||||
width=self.s1_thickness,
|
||||
thickness=self.spacer_thickness,
|
||||
holes=holes,
|
||||
centred=(True, True),
|
||||
hole_diam=self.shoulder_joint.child_conn_hole_diam,
|
||||
centre_left_right_tags=True,
|
||||
centre_bot_top_tags=True,
|
||||
)
|
||||
@submodel(name="spacer-s1-elbow")
|
||||
def spacer_s1_elbow(self) -> MountingBox:
|
||||
|
@ -664,8 +682,7 @@ class WingProfile(Model):
|
|||
param=self.s1_thickness)
|
||||
)
|
||||
for o, t in [
|
||||
(self.spacer_s1_shoulder(), "shoulder_bot"),
|
||||
(self.spacer_s1_shoulder(), "shoulder_top"),
|
||||
(self.spacer_s1_shoulder(), "shoulder"),
|
||||
(self.spacer_s1_elbow(), "elbow_top"),
|
||||
(self.spacer_s1_elbow(), "elbow_bot"),
|
||||
(self.spacer_s1_elbow_act(), "elbow_act"),
|
||||
|
@ -988,13 +1005,8 @@ class WingProfile(Model):
|
|||
if "s1" in parts:
|
||||
result.add(self.assembly_s1(), name="s1")
|
||||
if "s1" in parts and "shoulder" in parts:
|
||||
(
|
||||
result
|
||||
.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")
|
||||
)
|
||||
for i in range(len(self.shoulder_joint.child_conn_hole_pos)):
|
||||
result.constrain(f"s1/shoulder?conn{i}", f"shoulder/child/lip?conn{i}", "Plane")
|
||||
if "elbow" in parts:
|
||||
angle = self.elbow_joint.motion_span * elbow_wrist_deflection
|
||||
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_rotate = -self.wrist_joint.angle_neutral
|
||||
self.wrist_joint.flip = False
|
||||
self.shoulder_joint.flip = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
|
|
Loading…
Reference in New Issue