""" 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.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_thickness: float = 25.4 / 8 harness_width: float = 300 harness_height: float = 400 harness_fillet: float = 10 harness_wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ ("r1", 70, 150), ("l1", -70, 150), ("r2", 100, 0), ("l2", -100, 0), ("r3", 70, -150), ("l3", -70, -150), ]) # Holes drilled onto harness for attachment with HS joint harness_to_root_conn_diam: float = 6 hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=30, radius_inner=20, tooth_height=10, base_height=5 )) # Wing root properties # # The Houjuu-Scarlett joint mechanism at the base of the wing hs_joint_base_width: float = 85 hs_joint_base_thickness: float = 10 hs_joint_corner_fillet: float = 5 hs_joint_corner_cbore_diam: float = 12 hs_joint_corner_cbore_depth: float = 2 hs_joint_corner_inset: float = 12 hs_joint_axis_diam: float = 12 hs_joint_axis_cbore_diam: float = 20 hs_joint_axis_cbore_depth: float = 3 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") 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").hole(self.trident_terminal_hole_diam, depth=h) return result def harness_profile(self) -> Cq.Sketch: """ Creates the harness shape """ w, h = self.harness_width / 2, self.harness_height / 2 sketch = ( Cq.Sketch() .polygon([ (0.7 * w, h), (w, 0), (0.7 * w, -h), (0.7 * -w, -h), (-w, 0), (0.7 * -w, h), ]) #.rect(self.harness_width, self.harness_height) .vertices() .fillet(self.harness_fillet) ) for tag, x, y in self.harness_wing_base_pos: conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] sketch = ( sketch .push(conn) .tag(tag) .circle(self.harness_to_root_conn_diam / 2, mode='s') .reset() ) return sketch @target(name="harness", kind=TargetKind.DXF) def harness(self) -> Cq.Shape: """ Creates the harness shape """ result = ( Cq.Workplane('XZ') .placeSketch(self.harness_profile()) .extrude(self.harness_thickness) ) result.faces(">Y").tag("mount") plane = result.faces(">Y").workplane() for tag, x, y in self.harness_wing_base_pos: conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] for i, (px, py) in enumerate(conn): ( plane .moveTo(px, py) .circle(1, forConstruction='True') .edges() .tag(f"{tag}_{i}") ) return result def hs_joint_harness_conn(self) -> list[tuple[int, int]]: """ Generates a set of points corresponding to the connectorss """ dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset return [ (dx, dx), (dx, -dx), (-dx, -dx), (-dx, dx), ] @target(name="hs_joint_parent") def hs_joint_parent(self): """ Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth coupling, a cylindrical base, and a mounting base. """ hirth = self.hs_hirth_joint.generate() conn = self.hs_joint_harness_conn() result = ( Cq.Workplane('XY') .box( self.hs_joint_base_width, self.hs_joint_base_width, self.hs_joint_base_thickness, centered=(True, True, False)) .translate((0, 0, -self.hs_joint_base_thickness)) .edges("|Z") .fillet(self.hs_joint_corner_fillet) .faces(">Z") .workplane() .pushPoints(conn) .cboreHole( diameter=self.harness_to_root_conn_diam, cboreDiameter=self.hs_joint_corner_cbore_diam, cboreDepth=self.hs_joint_corner_cbore_depth) ) # Creates a plane parallel to the holes but shifted to the base plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) for i, (px, py) in enumerate(conn): ( plane .pushPoints([(px, py)]) .circle(1, forConstruction='True') .edges() .tag(f"h{i}") ) result = ( result .faces(">Z") .workplane() .union(hirth, tol=0.1) .clean() ) result = ( result.faces(" Cq.Assembly: harness = self.harness() result = ( Cq.Assembly() .add(harness, name="base", color=Material.WOOD_BIRCH.color) .constrain("base", "Fixed") ) for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: j = self.hs_joint_parent() ( result .add(j, name=name, color=Role.PARENT.color) .constrain("base?mount", f"{name}?base", "Axis") ) for i in range(4): result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") result.solve() return result #@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("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("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@ 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@ 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@ 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()