diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index ede9d9f..8f2d8a3 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -1,15 +1,42 @@ +""" +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 import unittest import cadquery as Cq import nhf.joints from nhf import Material +import nhf.touhou.houjuu_nue.wing as MW @dataclass(frozen=True) class Parameters: + """ + Defines dimensions for the Houjuu Nue cosplay + """ - """ - Thickness of the exoskeleton panel in millimetres - """ + # Thickness of the exoskeleton panel in millimetres panel_thickness: float = 25.4 / 16 # Harness @@ -28,12 +55,11 @@ class Parameters: ] # Holes drilled onto harness for attachment with HS joint - harness_to_wing_base_hole_diam = 6 + harness_to_root_conn_diam = 6 # Wing root properties - """ - The Houjuu-Scarlett joint mechanism at the base of the wing - """ + # + # The Houjuu-Scarlett joint mechanism at the base of the wing hs_joint_base_width = 85 hs_joint_base_thickness = 10 hs_joint_ring_thickness = 5 @@ -49,12 +75,8 @@ class Parameters: hs_joint_axis_cbore_diam = 20 hs_joint_axis_cbore_depth = 3 - - """ - Radius of the mounting mechanism of the wing root. This is constrained by - the size of the harness. - """ - root_radius: float = 60 + # Exterior radius of the wing root assembly + wing_root_radius = 40 """ Heights for various wing joints, where the numbers start from the first joint. @@ -64,6 +86,10 @@ class Parameters: wing_r2_height = 100 wing_r3_height = 100 + def __post_init__(self): + assert self.wing_root_radius > self.hs_joint_radius,\ + "Wing root must be large enough to accomodate joint" + def print_geometries(self): return [ @@ -95,7 +121,7 @@ class Parameters: sketch .push(conn) .tag(tag) - .circle(self.harness_to_wing_base_hole_diam / 2, mode='s') + .circle(self.harness_to_root_conn_diam / 2, mode='s') .reset() ) return sketch @@ -171,7 +197,7 @@ class Parameters: .workplane() .pushPoints(conn) .cboreHole( - diameter=self.harness_to_wing_base_hole_diam, + diameter=self.harness_to_root_conn_diam, cboreDiameter=self.hs_joint_corner_cbore_diam, cboreDepth=self.hs_joint_corner_cbore_depth) ) @@ -245,23 +271,16 @@ class Parameters: ) return result - def wing_root(self, - side_width=30, - side_height=100) -> Cq.Shape: + def wing_root(self) -> Cq.Shape: """ Generate the wing root which contains a Hirth joint at its base and a rectangular opening on its side, with the necessary interfaces. """ - result = ( - Cq.Workplane("XY") - .circle(self.root_radius) - .transformed(offset=Cq.Vector(80, 0, 80), - rotate=Cq.Vector(0, 45, 0)) - .rect(side_width, side_height) - .loft() - .val() - ) - return result + return MW.wing_base().val()#self.wing_root_radius) + + ###################### + # Assemblies # + ###################### def harness_assembly(self): harness = self.harness() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py new file mode 100644 index 0000000..a6fa011 --- /dev/null +++ b/nhf/touhou/houjuu_nue/wing.py @@ -0,0 +1,162 @@ +import math +import cadquery as Cq + +def wing_root_profiles( + base_sweep=150, + wall_thickness=8, + base_radius=60, + middle_offset=30, + conn_width=40, + conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: + assert base_sweep < 180 + assert middle_offset > 0 + theta = math.pi * base_sweep / 180 + c, s = math.cos(theta), math.sin(theta) + c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75) + c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2) + r1 = base_radius + r2 = base_radius - wall_thickness + base = ( + Cq.Sketch() + .arc( + (c * r1, s * r1), + (c_1 * r1, s_1 * r1), + (c_2 * r1, s_2 * r1), + ) + .arc( + (c_2 * r1, s_2 * r1), + (r1, 0), + (c_2 * r1, -s_2 * r1), + ) + .arc( + (c_2 * r1, -s_2 * r1), + (c_1 * r1, -s_1 * r1), + (c * r1, -s * r1), + ) + .segment( + (c * r1, -s * r1), + (c * r2, -s * r2), + ) + .arc( + (c * r2, -s * r2), + (c_1 * r2, -s_1 * r2), + (c_2 * r2, -s_2 * r2), + ) + .arc( + (c_2 * r2, -s_2 * r2), + (r2, 0), + (c_2 * r2, s_2 * r2), + ) + .arc( + (c_2 * r2, s_2 * r2), + (c_1 * r2, s_1 * r2), + (c * r2, s * r2), + ) + .segment( + (c * r2, s * r2), + (c * r1, s * r1), + ) + .assemble(tag="wire") + .wires().val() + ) + assert isinstance(base, Cq.Wire) + + # The interior sweep is given by theta, but the exterior sweep exceeds the + # interior sweep so the wall does not become thinner towards the edges. + # If the exterior sweep is theta', it has to satisfy + # + # sin(theta) * r2 + wall_thickness = sin(theta') * r1 + x, y = conn_width / 2, conn_height / 2 + t = wall_thickness + dx = middle_offset + middle = ( + Cq.Sketch() + # Interior arc, top point + .arc( + (x - t, y - t), + (x - t + dx, 0), + (x - t, -y + t), + ) + .segment( + (x - t, -y + t), + (-x, -y+t) + ) + .segment((-x, -y)) + .segment((x, -y)) + # Outer arc, bottom point + .arc( + (x, -y), + (x + dx, 0), + (x, y), + ) + .segment( + (x, y), + (-x, y) + ) + .segment((-x, y-t)) + #.segment((x2, a)) + .close() + .assemble(tag="wire") + .wires().val() + ) + assert isinstance(middle, Cq.Wire) + + x, y = conn_width / 2, conn_height / 2 + t = wall_thickness + tip = ( + Cq.Sketch() + .segment((-x, y), (x, y)) + .segment((x, -y)) + .segment((-x, -y)) + .segment((-x, -y+t)) + .segment((x-t, -y+t)) + .segment((x-t, y-t)) + .segment((-x, y-t)) + .close() + .assemble(tag="wire") + .wires().val() + ) + return base, middle, tip +def wing_base(): + root_profile, middle_profile, tip_profile = wing_root_profiles() + + rotate_centre = Cq.Vector(-200, 0, -25) + rotate_axis = Cq.Vector(0, 1, 0) + terminal_offset = Cq.Vector(-80, 0, 80) + terminal_rotate = Cq.Vector(0, -45, 0) + + #middle_profile = middle_profile.moved(Cq.Location((0, 0, -100))) + #tip_profile = tip_profile.moved(Cq.Location((0, 0, -200))) + middle_profile = middle_profile.rotate( + startVector=rotate_centre, + endVector=rotate_centre + rotate_axis, + angleDegrees = 35, + ) + tip_profile = tip_profile.rotate( + startVector=rotate_centre, + endVector=rotate_centre + rotate_axis, + angleDegrees = 70, + ) + seg1 = ( + Cq.Workplane('XY') + .add(root_profile) + .toPending() + .transformed( + offset=terminal_offset, + rotate=terminal_rotate) + #.add(middle_profile.moved(Cq.Location((-15, 0, 15)))) + .add(middle_profile) + .toPending() + .loft() + ) + seg2 = ( + Cq.Workplane('XY') + .add(middle_profile) + .toPending() + .workplane() + .add(tip_profile) + .toPending() + .loft() + ) + seg1 = seg1.union(seg2) + return seg1