""" This file describes the shapes of the wing shells. The joints are defined in `__init__.py`. """ import math from enum import Enum from dataclasses import dataclass, field from typing import Mapping, Tuple, Optional import cadquery as Cq from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint, DiskJoint import nhf.utils @dataclass(kw_only=True) class WingProfile(Model): name: str = "wing" base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=30.0, radius_inner=20.0, )) root_width: float = 80.0 hs_joint_corner_dx: float = 30.0 hs_joint_corner_hole_diam: float = 6.0 panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( height=60.0, )) shoulder_width: float = 30.0 shoulder_tip_x: float = -200.0 shoulder_tip_y: float = 160.0 s1_thickness: float = 25.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=55, ), flip=False, )) s2_thickness: float = 25.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=45, ), flip=True, )) s3_thickness: float = 25.0 mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT mat_bracket: Material = Material.ACRYLIC_TRANSPARENT mat_hs_joint: Material = Material.PLASTIC_PLA role_panel: Role = Role.STRUCTURE # Subclass must populate elbow_x: float elbow_y: float elbow_angle: float elbow_height: float wrist_x: float wrist_y: float wrist_angle: float wrist_height: float flip: bool = False def __post_init__(self): super().__init__(name=self.name) self.elbow_theta = math.radians(self.elbow_angle) self.elbow_c = math.cos(self.elbow_theta) self.elbow_s = math.sin(self.elbow_theta) self.elbow_top_x, self.elbow_top_y = self.elbow_to_abs(0, self.elbow_height) self.wrist_theta = math.radians(self.wrist_angle) self.wrist_c = math.cos(self.wrist_theta) self.wrist_s = math.sin(self.wrist_theta) self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) @property def root_height(self) -> float: return self.shoulder_joint.height @property def shoulder_height(self) -> float: return self.shoulder_joint.height @target(name="profile-s0", kind=TargetKind.DXF) def profile_s0(self) -> Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y sketch = ( Cq.Sketch() .segment((-self.root_width, 0), (0, 0)) .spline([ (0, 0), (-30.0, 80.0), (tip_x, tip_y) ]) .segment( (tip_x, tip_y), (tip_x, tip_y - self.shoulder_width) ) .segment( (tip_x, tip_y - self.shoulder_width), (-self.root_width, 0) ) .assemble() ) return sketch @submodel(name="spacer-s0-shoulder") def spacer_s0_shoulder(self) -> MountingBox: """ Should be cut """ holes = [ hole for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos) for hole in [ Hole(x=x, tag=f"conn_top{i}"), Hole(x=-x, tag=f"conn_bot{i}"), ] ] return MountingBox( length=self.shoulder_joint.height, width=self.shoulder_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.shoulder_joint.parent_conn_hole_diam, centred=(True, True), flip_y=self.flip, ) @submodel(name="spacer-s0-shoulder") def spacer_s0_base(self) -> MountingBox: """ Should be cut """ dx = self.hs_joint_corner_dx holes = [ Hole(x=-dx, y=-dx), Hole(x=dx, y=-dx), Hole(x=dx, y=dx), Hole(x=-dx, y=dx), ] return MountingBox( length=self.root_height, width=self.root_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.hs_joint_corner_hole_diam, centred=(True, True), flip_y=self.flip, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: tags = [ ("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0), ("base", (0, 0), 90), ] return nhf.utils.extrude_with_markers( self.profile_s0(), self.panel_thickness, tags, reverse=not top, ) @assembly() def assembly_s0(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s0(top=True), name="bot", material=self.mat_panel, role=self.role_panel) .addS(self.surface_s0(top=False), name="top", material=self.mat_panel, role=self.role_panel) .constrain("bot@faces@>Z", "top@faces@ Cq.Sketch: """ Generates profile from shoulder and above """ def _assembly_insert_spacer( self, a: Cq.Assembly, spacer: Cq.Workplane, point_tag: str, front_tag: str = "front", back_tag: str = "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 ( a .addS( spacer, name=point_tag, material=self.mat_bracket, role=self.role_panel) .constrain(f"{front_tag}?{point_tag}", f"{point_tag}?{site_front}", "Plane") .constrain(f"{back_tag}?{point_tag}", f"{point_tag}?{site_back}", "Plane") .constrain(f"{point_tag}?dir", f"{front_tag}?{point_tag}_dir", "Axis", param=angle) ) def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c return elbow_x, elbow_y def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]: wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s wrist_y = self.wrist_y + x * self.wrist_s + y * self.wrist_c return wrist_x, wrist_y def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow """ l = 200 return [ (0, -l), (self.elbow_x, -l), (self.elbow_x, self.elbow_y), (self.elbow_top_x, self.elbow_top_y), (self.elbow_top_x, l), (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 return [ (0, -l), (self.wrist_x, -l), (self.wrist_x, self.wrist_y), (self.wrist_top_x, self.wrist_top_y), (self.wrist_top_x, l), (0, l), ] @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_elbow(), mode='i') ) return profile def surface_s1(self, shoulder_mount_inset: float = 0, elbow_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: shoulder_h = self.shoulder_joint.child_height h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ ("shoulder_bot", (shoulder_mount_inset, h), 90), ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270), ] elbow_h = self.elbow_joint.parent_beam.total_height h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(-elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", self.elbow_to_abs(-elbow_mount_inset, h + elbow_h), self.elbow_angle + 270), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: holes = [ Hole(x) for x in self.shoulder_joint.child_conn_hole_pos ] return MountingBox( length=50.0, # FIXME: magic width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=self.shoulder_joint.child_conn_hole_diam, ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: holes = [ Hole(x) for x in self.elbow_joint.parent_hole_pos() ] return MountingBox( length=70.0, # FIXME: magic width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=self.elbow_joint.hole_diam, ) @assembly() def assembly_s1(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s1(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s1(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_elbow(), mode='s') .reset() .polygon(self._mask_wrist(), mode='i') ) return profile def surface_s2(self, thickness: float = 25.4/16, elbow_mount_inset: float = 0, wrist_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: elbow_h = self.elbow_joint.child_beam.total_height h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", self.elbow_to_abs(elbow_mount_inset, h + elbow_h), self.elbow_angle - 90), ] wrist_h = self.wrist_joint.parent_beam.total_height h = (self.wrist_height - wrist_h) / 2 tags_wrist = [ ("wrist_bot", self.wrist_to_abs(-wrist_mount_inset, h), self.wrist_angle + 90), ("wrist_top", self.wrist_to_abs(-wrist_mount_inset, h + wrist_h), self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: holes = [ Hole(x) for x in self.elbow_joint.child_hole_pos() ] return MountingBox( length=50.0, # FIXME: magic width=self.s2_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=self.elbow_joint.hole_diam, ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: holes = [ Hole(x) for x in self.wrist_joint.parent_hole_pos() ] return MountingBox( length=70.0, # FIXME: magic width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=self.wrist_joint.hole_diam, ) @assembly() def assembly_s2(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s2(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s2(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_wrist(), mode='s') ) return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: wrist_mount_inset = 0 wrist_h = self.wrist_joint.child_beam.total_height h = (self.wrist_height - wrist_h) / 2 tags = [ ("wrist_bot", self.wrist_to_abs(wrist_mount_inset, h), self.wrist_angle + 90), ("wrist_top", self.wrist_to_abs(wrist_mount_inset, h + wrist_h), self.wrist_angle - 90), ] profile = self.profile_s3() return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: holes = [ Hole(x) for x in self.wrist_joint.child_hole_pos() ] return MountingBox( length=70.0, # FIXME: magic width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, hole_diam=self.wrist_joint.hole_diam ) @assembly() def assembly_s3(self) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s3(front=True), name="front", material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") .addS(self.surface_s3(front=False), name="back", material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Assembly(): assert not self.elbow_joint.flip assert self.wrist_joint.flip if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( Cq.Assembly() ) if "s0" in parts: result.add(self.assembly_s0(), name="s0") if "shoulder" in parts: result.add(self.shoulder_joint.assembly(), name="shoulder") if "s0" in parts and "shoulder" in parts: ( result .constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane") .constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") .constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane") .constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") ) 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", "shoulder/child/lip_top?conn0", "Plane") .constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane") .constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane") .constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane") ) if "elbow" in parts: result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow") if "s1" in parts and "elbow" in parts: ( result .constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane") .constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane") .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane") .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane") ) if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: ( result .constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane") .constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane") .constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane") .constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane") ) if "wrist" in parts: result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist") if "s2" in parts and "wrist" in parts: # Mounted backwards to bend in other direction ( result .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") ) if len(parts) > 1: result.solve() return result @dataclass(kw_only=True) class WingR(WingProfile): """ Right side wings """ elbow_height: float = 111.0 elbow_x: float = 363.0 elbow_y: float = 44.0 # Tilt of elbow w.r.t. shoulder elbow_angle: float = 30.0 wrist_height: float = 60.0 # Bottom point of the wrist wrist_x: float = 403.0 wrist_y: float = 253.0 # Tile of wrist w.r.t. shoulder wrist_angle: float = 40 # Extends from the wrist to the tip of the arrow arrow_height: float = 300 arrow_angle: float = 8 # Relative (in wrist coordinate) centre of the ring ring_x: float = 45 ring_y: float = 25 ring_radius_inner: float = 22 def __post_init__(self): super().__post_init__() self.arrow_theta = math.radians(self.arrow_angle) self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height) self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \ * math.sin(self.arrow_theta - self.wrist_theta) self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \ * math.cos(self.arrow_theta - self.wrist_theta) # [[c, s], [-s, c]] * [ring_x, ring_y] self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y assert self.ring_radius > self.ring_radius_inner @property def ring_radius(self) -> float: dx = self.ring_x dy = self.ring_y return (dx * dx + dy * dy) ** 0.5 def profile(self) -> Cq.Sketch: """ Net profile of the wing starting from the wing root with no divisions """ result = ( Cq.Sketch() .segment( (0, 0), (0, self.shoulder_joint.height), tag="shoulder") .spline([ (0, self.shoulder_joint.height), (self.elbow_top_x, self.elbow_top_y), (self.wrist_top_x, self.wrist_top_y), ], tag="s1_top") #.segment( # (self.wrist_x, self.wrist_y), # (wrist_top_x, wrist_top_y), # tag="wrist") .spline([ (0, 0), (self.elbow_x, self.elbow_y), (self.wrist_x, self.wrist_y), ], tag="s1_bot") ) result = ( result .segment( (self.wrist_x, self.wrist_y), (self.arrow_x, self.arrow_y) ) .segment( (self.arrow_x, self.arrow_y), (self.arrow_tip_x, self.arrow_tip_y) ) .segment( (self.arrow_tip_x, self.arrow_tip_y), (self.wrist_top_x, self.wrist_top_y) ) ) # Carve out the ring result = result.assemble() result = ( result .push([(self.ring_abs_x, self.ring_abs_y)]) .circle(self.ring_radius, mode='a') .circle(self.ring_radius_inner, mode='s') .clean() ) return result @dataclass(kw_only=True) class WingL(WingProfile): elbow_x: float = 230.0 elbow_y: float = 110.0 elbow_angle: float = -10.0 elbow_height: float = 80.0 wrist_x: float = 480.0 wrist_y: float = 0.0 wrist_angle: float = -45 wrist_height: float = 43.0 shoulder_bezier_ext: float = 80.0 elbow_bezier_ext: float = 100.0 wrist_bezier_ext: float = 30.0 arrow_length: float = 135.0 arrow_height: float = 120.0 flip: bool = True def __post_init__(self): super().__post_init__() assert self.wrist_height <= self.shoulder_joint.height def arrow_to_abs(self, x, y) -> Tuple[float, float]: return self.wrist_to_abs(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2) def profile(self) -> Cq.Sketch: result = ( Cq.Sketch() .segment( (0,0), (0, self.shoulder_height) ) #.spline([ # (0, 0), # self.elbow_to_abs(0, 0), # self.wrist_to_abs(0, 0), #]) #.spline([ # (0, self.shoulder_height), # self.elbow_to_abs(0, self.elbow_height), # self.wrist_to_abs(0, self.wrist_height), #]) .bezier([ (0, 0), (self.shoulder_bezier_ext, 0), self.elbow_to_abs(-self.elbow_bezier_ext, 0), self.elbow_to_abs(0, 0), ]) .bezier([ (0, self.shoulder_joint.height), (self.shoulder_bezier_ext, self.shoulder_joint.height), self.elbow_to_abs(-self.elbow_bezier_ext, self.elbow_height), self.elbow_to_abs(0, self.elbow_height), ]) .bezier([ self.elbow_to_abs(0, 0), self.elbow_to_abs(self.elbow_bezier_ext, 0), self.wrist_to_abs(-self.wrist_bezier_ext, 0), self.wrist_to_abs(0, 0), ]) .bezier([ self.elbow_to_abs(0, self.elbow_height), self.elbow_to_abs(self.elbow_bezier_ext, self.elbow_height), self.wrist_to_abs(-self.wrist_bezier_ext, self.wrist_height), self.wrist_to_abs(0, self.wrist_height), ]) ) # arrow base positions base_u, base_v = 0.3, 0.3 result = ( result .bezier([ self.wrist_to_abs(0, self.wrist_height), self.wrist_to_abs(self.wrist_bezier_ext, self.wrist_height), self.arrow_to_abs(base_u, base_v), ]) .bezier([ self.wrist_to_abs(0, 0), self.wrist_to_abs(self.wrist_bezier_ext, 0), self.arrow_to_abs(base_u, -base_v), ]) ) # Create the arrow arrow_beziers = [ [ (0, 1), (0.3, 1), (0.8, .2), (1, 0), ], [ (0, 1), (0.1, 0.8), (base_u, base_v), ] ] arrow_beziers = [ l2 for l in arrow_beziers for l2 in [l, [(x, -y) for x,y in l]] ] for line in arrow_beziers: result = result.bezier([self.arrow_to_abs(x, y) for x,y in line]) return result.assemble()