484 lines
17 KiB
Python
484 lines
17 KiB
Python
"""
|
|
To build, execute
|
|
```
|
|
python3 nhf/touhou/houjuu_nue/__init__.py
|
|
```
|
|
|
|
This cosplay consists of 3 components:
|
|
|
|
## Trident
|
|
|
|
The trident is composed of individual segments, made of acrylic, and a 3D
|
|
printed head (convention rule prohibits metal) with a metallic paint. To ease
|
|
transportation, the trident handle has individual segments with threads and can
|
|
be assembled on site.
|
|
|
|
## Snake
|
|
|
|
A 3D printed snake with a soft material so it can wrap around and bend
|
|
|
|
## Wings
|
|
|
|
This is the crux of the cosplay and the most complex component. The wings mount
|
|
on a wearable harness. Each wing consists of 4 segments with 3 joints. Parts of
|
|
the wing which demands transluscency are created from 1/16" acrylic panels.
|
|
These panels serve double duty as the exoskeleton.
|
|
|
|
The wings are labeled r1,r2,r3,l1,l2,l3. The segments of the wings are labeled
|
|
from root to tip s0 (root),
|
|
s1, s2, s3. The joints are named (from root to tip)
|
|
shoulder, elbow, wrist in analogy with human anatomy.
|
|
"""
|
|
from dataclasses import dataclass, field
|
|
import cadquery as Cq
|
|
from nhf import Material, Role
|
|
from nhf.build import Model, TargetKind, target, assembly
|
|
from nhf.parts.joints import HirthJoint, TorsionJoint
|
|
from nhf.parts.handle import Handle, BayonetMount
|
|
import nhf.touhou.houjuu_nue.wing as MW
|
|
import nhf.touhou.houjuu_nue.trident as MT
|
|
import nhf.touhou.houjuu_nue.joints as MJ
|
|
import nhf.touhou.houjuu_nue.harness as MH
|
|
import nhf.utils
|
|
|
|
@dataclass
|
|
class Parameters(Model):
|
|
"""
|
|
Defines dimensions for the Houjuu Nue cosplay
|
|
"""
|
|
|
|
# Thickness of the exoskeleton panel in millimetres
|
|
panel_thickness: float = 25.4 / 16
|
|
|
|
# Harness
|
|
harness: MH.Harness = field(default_factory=lambda: MH.Harness())
|
|
|
|
hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
|
|
radius=30,
|
|
radius_inner=20,
|
|
tooth_height=10,
|
|
base_height=5,
|
|
n_tooth=24
|
|
))
|
|
|
|
wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(
|
|
shoulder_height=100.0,
|
|
elbow_height=110.0,
|
|
))
|
|
|
|
# Exterior radius of the wing root assembly
|
|
wing_root_radius: float = 40
|
|
wing_root_wall_thickness: float = 8
|
|
|
|
shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint(
|
|
shoulder_height=100.0,
|
|
))
|
|
elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
|
|
))
|
|
wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint(
|
|
))
|
|
|
|
"""
|
|
Heights for various wing joints, where the numbers start from the first
|
|
joint.
|
|
"""
|
|
wing_s0_thickness: float = 40
|
|
|
|
# Length of the spacer
|
|
wing_s1_thickness: float = 20
|
|
wing_s1_spacer_thickness: float = 25.4 / 8
|
|
wing_s1_spacer_width: float = 20
|
|
wing_s1_spacer_hole_diam: float = 8
|
|
wing_s1_shoulder_spacer_hole_dist: float = 20
|
|
wing_s1_shoulder_spacer_width: float = 60
|
|
|
|
trident_handle: Handle = field(default_factory=lambda: Handle(
|
|
diam=38,
|
|
diam_inner=38-2 * 25.4/8,
|
|
diam_connector_internal=18,
|
|
simplify_geometry=False,
|
|
mount=BayonetMount(n_pin=3),
|
|
))
|
|
trident_terminal_height: float = 80
|
|
trident_terminal_hole_diam: float = 24
|
|
trident_terminal_bottom_thickness: float = 10
|
|
|
|
material_panel: Material = Material.ACRYLIC_TRANSPARENT
|
|
material_bracket: Material = Material.ACRYLIC_TRANSPARENT
|
|
|
|
def __post_init__(self):
|
|
super().__init__(name="houjuu-nue")
|
|
self.harness.hs_hirth_joint = self.hs_hirth_joint
|
|
assert self.wing_root_radius > self.hs_hirth_joint.radius, \
|
|
"Wing root must be large enough to accomodate joint"
|
|
assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \
|
|
"Spacer holes are too close to each other"
|
|
|
|
@target(name="trident/handle-connector")
|
|
def handle_connector(self):
|
|
return self.trident_handle.connector()
|
|
@target(name="trident/handle-insertion")
|
|
def handle_insertion(self):
|
|
return self.trident_handle.insertion()
|
|
@target(name="trident/proto-handle-connector", prototype=True)
|
|
def proto_handle_connector(self):
|
|
return self.trident_handle.one_side_connector(height=15)
|
|
@target(name="trident/handle-terminal-connector")
|
|
def handle_terminal_connector(self):
|
|
result = self.trident_handle.one_side_connector(height=self.trident_terminal_height)
|
|
#result.faces("<Z").circle(radius=25/2).cutThruAll()
|
|
h = self.trident_terminal_height + self.trident_handle.insertion_length - self.trident_terminal_bottom_thickness
|
|
result = result.faces(">Z").hole(self.trident_terminal_hole_diam, depth=h)
|
|
return result
|
|
|
|
@target(name="harness", kind=TargetKind.DXF)
|
|
def harness_profile(self) -> Cq.Sketch:
|
|
return self.harness.profile()
|
|
|
|
def harness_surface(self) -> Cq.Workplane:
|
|
return self.harness.surface()
|
|
|
|
def hs_joint_parent(self) -> Cq.Workplane:
|
|
return self.harness.hs_joint_parent()
|
|
|
|
@assembly()
|
|
def harness_assembly(self) -> Cq.Assembly:
|
|
return self.harness.assembly()
|
|
|
|
#@target(name="wing/joining-plate", kind=TargetKind.DXF)
|
|
#def joining_plate(self) -> Cq.Workplane:
|
|
# return self.wing_joining_plate.plate()
|
|
|
|
@target(name="wing/root")
|
|
def wing_root(self) -> Cq.Assembly:
|
|
"""
|
|
Generate the wing root which contains a Hirth joint at its base and a
|
|
rectangular opening on its side, with the necessary interfaces.
|
|
"""
|
|
return MW.wing_root(
|
|
joint=self.hs_hirth_joint,
|
|
shoulder_attach_dist=self.shoulder_joint.attach_dist,
|
|
shoulder_attach_diam=self.shoulder_joint.attach_diam,
|
|
wall_thickness=self.wing_root_wall_thickness,
|
|
conn_height=self.wing_profile.shoulder_height,
|
|
conn_thickness=self.wing_s0_thickness,
|
|
)
|
|
|
|
@target(name="wing/proto-shoulder-joint-parent", prototype=True)
|
|
def proto_shoulder_joint_parent(self):
|
|
return self.shoulder_joint.torsion_joint.track()
|
|
@target(name="wing/proto-shoulder-joint-child", prototype=True)
|
|
def proto_shoulder_joint_child(self):
|
|
return self.shoulder_joint.torsion_joint.rider()
|
|
@assembly()
|
|
def shoulder_assembly(self):
|
|
return self.shoulder_joint.assembly(
|
|
wing_root_wall_thickness=self.wing_root_wall_thickness,
|
|
lip_height=self.wing_s1_thickness,
|
|
hole_dist=self.wing_s1_shoulder_spacer_hole_dist,
|
|
spacer_hole_diam=self.wing_s1_spacer_hole_diam,
|
|
)
|
|
@assembly()
|
|
def elbow_assembly(self):
|
|
return self.elbow_joint.assembly()
|
|
@assembly()
|
|
def wrist_assembly(self):
|
|
return self.wrist_joint.assembly()
|
|
|
|
|
|
@target(name="wing/s1-spacer", kind=TargetKind.DXF)
|
|
def wing_s1_spacer(self) -> Cq.Workplane:
|
|
result = (
|
|
Cq.Workplane('XZ')
|
|
.sketch()
|
|
.rect(self.wing_s1_spacer_width, self.wing_s1_thickness)
|
|
.finalize()
|
|
.extrude(self.wing_s1_spacer_thickness)
|
|
)
|
|
result.faces("<Z").tag("weld1")
|
|
result.faces(">Z").tag("weld2")
|
|
result.faces(">Y").tag("dir")
|
|
return result
|
|
|
|
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
|
|
def wing_s1_shoulder_spacer(self) -> Cq.Workplane:
|
|
"""
|
|
Creates a rectangular spacer. This could be cut from acrylic.
|
|
|
|
There are two holes on the top of the spacer. With the holes
|
|
"""
|
|
dx = self.wing_s1_shoulder_spacer_hole_dist
|
|
h = self.wing_s1_spacer_thickness
|
|
length = self.wing_s1_shoulder_spacer_width
|
|
hole_diam = self.wing_s1_spacer_hole_diam
|
|
assert dx + hole_diam < length / 2
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.sketch()
|
|
.rect(length, self.wing_s1_thickness)
|
|
.push([
|
|
(0, 0),
|
|
(dx, 0),
|
|
])
|
|
.circle(hole_diam / 2, mode='s')
|
|
.finalize()
|
|
.extrude(h)
|
|
)
|
|
# Tag the mating surfaces to be glued
|
|
result.faces("<Y").workplane().moveTo(length / 2, h).tagPlane("left")
|
|
result.faces(">Y").workplane().moveTo(-length / 2, h).tagPlane("right")
|
|
|
|
# Tag the directrix
|
|
result.faces(">Z").tag("dir")
|
|
|
|
# Tag the holes
|
|
plane = result.faces(">Z").workplane()
|
|
# Side closer to the parent is 0
|
|
plane.moveTo(dx, 0).tagPlane("conn0")
|
|
plane.tagPlane("conn1")
|
|
return result
|
|
def assembly_insert_shoulder_spacer(
|
|
self,
|
|
assembly,
|
|
spacer,
|
|
point_tag: str,
|
|
front_tag: str = "panel_front",
|
|
back_tag: str = "panel_back",
|
|
flipped: bool = False,
|
|
):
|
|
"""
|
|
For a child joint facing up, front panel should be on the right, back
|
|
panel on the left
|
|
"""
|
|
site_front, site_back = "right", "left"
|
|
if flipped:
|
|
site_front, site_back = site_back, site_front
|
|
angle = 0
|
|
(
|
|
assembly
|
|
.add(spacer,
|
|
name=f"{point_tag}_spacer",
|
|
color=self.material_bracket.color)
|
|
.constrain(f"{front_tag}?{point_tag}",
|
|
f"{point_tag}_spacer?{site_front}", "Plane")
|
|
.constrain(f"{back_tag}?{point_tag}",
|
|
f"{point_tag}_spacer?{site_back}", "Plane")
|
|
.constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir",
|
|
"Axis", param=angle)
|
|
)
|
|
|
|
@target(name="wing/r1s1", kind=TargetKind.DXF)
|
|
def wing_r1s1_profile(self) -> Cq.Sketch:
|
|
"""
|
|
FIXME: Output individual segment profiles
|
|
"""
|
|
return self.wing_profile.profile()
|
|
|
|
def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
|
|
return self.wing_profile.surface_s1(
|
|
thickness=self.panel_thickness,
|
|
shoulder_joint_child_height=self.shoulder_joint.child_height,
|
|
front=front,
|
|
)
|
|
def wing_r1s2_panel(self, front=True) -> Cq.Workplane:
|
|
return self.wing_profile.surface_s2(
|
|
thickness=self.panel_thickness,
|
|
front=front,
|
|
)
|
|
def wing_r1s3_panel(self, front=True) -> Cq.Workplane:
|
|
return self.wing_profile.surface_s3(
|
|
thickness=self.panel_thickness,
|
|
front=front,
|
|
)
|
|
|
|
@assembly()
|
|
def wing_r1s1_assembly(self) -> Cq.Assembly:
|
|
result = (
|
|
Cq.Assembly()
|
|
.add(self.wing_r1s1_panel(front=True), name="panel_front",
|
|
color=self.material_panel.color)
|
|
.constrain("panel_front", "Fixed")
|
|
.add(self.wing_r1s1_panel(front=False), name="panel_back",
|
|
color=self.material_panel.color)
|
|
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
|
|
param=self.wing_s1_thickness)
|
|
)
|
|
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
|
|
is_top = t.endswith("_top")
|
|
is_parent = t.startswith("shoulder")
|
|
self.assembly_insert_shoulder_spacer(
|
|
result,
|
|
self.wing_s1_shoulder_spacer(),
|
|
point_tag=t,
|
|
flipped=is_top == is_parent,
|
|
)
|
|
return result.solve()
|
|
@assembly()
|
|
def wing_r1s2_assembly(self) -> Cq.Assembly:
|
|
result = (
|
|
Cq.Assembly()
|
|
.add(self.wing_r1s2_panel(front=True), name="panel_front",
|
|
color=self.material_panel.color)
|
|
.constrain("panel_front", "Fixed")
|
|
.add(self.wing_r1s2_panel(front=False), name="panel_back",
|
|
color=self.material_panel.color)
|
|
# FIXME: Use s2 thickness
|
|
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
|
|
param=self.wing_s1_thickness)
|
|
)
|
|
for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]:
|
|
is_top = t.endswith("_top")
|
|
is_parent = t.startswith("elbow")
|
|
self.assembly_insert_shoulder_spacer(
|
|
result,
|
|
self.wing_s1_shoulder_spacer(),
|
|
point_tag=t,
|
|
flipped=is_top == is_parent,
|
|
)
|
|
return result.solve()
|
|
@assembly()
|
|
def wing_r1s3_assembly(self) -> Cq.Assembly:
|
|
result = (
|
|
Cq.Assembly()
|
|
.add(self.wing_r1s3_panel(front=True), name="panel_front",
|
|
color=self.material_panel.color)
|
|
.constrain("panel_front", "Fixed")
|
|
.add(self.wing_r1s3_panel(front=False), name="panel_back",
|
|
color=self.material_panel.color)
|
|
# FIXME: Use s2 thickness
|
|
.constrain("panel_front@faces@>Z", "panel_back@faces@<Z", "Point",
|
|
param=self.wing_s1_thickness)
|
|
)
|
|
for t in ["wrist_bot", "wrist_top"]:
|
|
self.assembly_insert_shoulder_spacer(
|
|
result,
|
|
self.wing_s1_shoulder_spacer(),
|
|
point_tag=t
|
|
)
|
|
return result.solve()
|
|
|
|
|
|
@assembly()
|
|
def wing_r1_assembly(
|
|
self,
|
|
parts=["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"],
|
|
) -> Cq.Assembly:
|
|
result = (
|
|
Cq.Assembly()
|
|
)
|
|
if "s0" in parts:
|
|
(
|
|
result
|
|
.add(self.wing_root(), name="s0")
|
|
.constrain("s0/scaffold", "Fixed")
|
|
)
|
|
if "shoulder" in parts:
|
|
result.add(self.shoulder_assembly(), name="shoulder")
|
|
|
|
if "s0" in parts and "shoulder" in parts:
|
|
(
|
|
result
|
|
.constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
|
|
.constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
|
|
.constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
|
|
.constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
|
|
)
|
|
|
|
if "s1" in parts:
|
|
result.add(self.wing_r1s1_assembly(), name="s1")
|
|
|
|
if "s1" in parts and "shoulder" in parts:
|
|
(
|
|
result
|
|
.constrain("shoulder/child/lip_bot?conn0",
|
|
"s1/shoulder_bot_spacer?conn0",
|
|
"Plane")
|
|
.constrain("shoulder/child/lip_bot?conn1",
|
|
"s1/shoulder_bot_spacer?conn1",
|
|
"Plane")
|
|
.constrain("shoulder/child/lip_top?conn0",
|
|
"s1/shoulder_top_spacer?conn0",
|
|
"Plane")
|
|
.constrain("shoulder/child/lip_top?conn1",
|
|
"s1/shoulder_top_spacer?conn1",
|
|
"Plane")
|
|
)
|
|
if "elbow" in parts:
|
|
result.add(self.elbow_assembly(), name="elbow")
|
|
|
|
if "s2" in parts:
|
|
result.add(self.wing_r1s2_assembly(), name="s2")
|
|
|
|
if "s1" in parts and "elbow" in parts:
|
|
(
|
|
result
|
|
.constrain("elbow/parent_upper/top?conn1",
|
|
"s1/elbow_top_spacer?conn1",
|
|
"Plane")
|
|
.constrain("elbow/parent_upper/top?conn0",
|
|
"s1/elbow_top_spacer?conn0",
|
|
"Plane")
|
|
.constrain("elbow/parent_upper/bot?conn1",
|
|
"s1/elbow_bot_spacer?conn1",
|
|
"Plane")
|
|
.constrain("elbow/parent_upper/bot?conn0",
|
|
"s1/elbow_bot_spacer?conn0",
|
|
"Plane")
|
|
)
|
|
|
|
if "s2" in parts and "elbow" in parts:
|
|
(
|
|
result
|
|
.constrain("elbow/child/bot?conn0",
|
|
"s2/elbow_bot_spacer?conn0",
|
|
"Plane")
|
|
.constrain("elbow/child/bot?conn1",
|
|
"s2/elbow_bot_spacer?conn1",
|
|
"Plane")
|
|
.constrain("elbow/child/top?conn0",
|
|
"s2/elbow_top_spacer?conn0",
|
|
"Plane")
|
|
.constrain("elbow/child/top?conn1",
|
|
"s2/elbow_top_spacer?conn1",
|
|
"Plane")
|
|
)
|
|
if len(parts) > 1:
|
|
result.solve()
|
|
return result
|
|
|
|
@assembly()
|
|
def wings_assembly(self) -> Cq.Assembly:
|
|
"""
|
|
Assembly of harness with all the wings
|
|
"""
|
|
a_tooth = self.hs_hirth_joint.tooth_angle
|
|
|
|
result = (
|
|
Cq.Assembly()
|
|
.add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
|
|
.add(self.wing_root(), name="w0_r1")
|
|
.add(self.wing_root(), name="w0_l1")
|
|
.constrain("harness/base", "Fixed")
|
|
.constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane")
|
|
.constrain("w0_r1/joint?dir", "harness/r1?dir",
|
|
"Axis", param=7 * a_tooth)
|
|
.constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane")
|
|
.constrain("w0_l1/joint?dir", "harness/l1?dir",
|
|
"Axis", param=-1 * a_tooth)
|
|
.solve()
|
|
)
|
|
return result
|
|
|
|
@assembly(collision_check=False)
|
|
def trident_assembly(self) -> Cq.Assembly:
|
|
"""
|
|
Disable collision check since the threads may not align.
|
|
"""
|
|
return MT.trident_assembly(self.trident_handle)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
p = Parameters()
|
|
p.build_all()
|