cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
2 changed files with 100 additions and 43 deletions
Showing only changes of commit b8c6fb51fd - Show all commits

View File

@ -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

View File

@ -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__()