""" 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 from typing import Mapping, Tuple import cadquery as Cq from nhf import Material, Role from nhf.parts.joints import HirthJoint import nhf.utils def wing_root_profiles( base_sweep=150, wall_thickness=8, base_radius=40, middle_offset=30, middle_height=80, conn_thickness=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_thickness / 2, middle_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_thickness / 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_root(joint: HirthJoint, bolt_diam: int = 12, union_tol=1e-4, shoulder_attach_diam=8, shoulder_attach_dist=25, conn_thickness=40, conn_height=100, wall_thickness=8) -> Cq.Assembly: """ Generate the contiguous components of the root wing segment """ tip_centre = Cq.Vector((-150, 0, -80)) attach_theta = math.radians(5) c, s = math.cos(attach_theta), math.sin(attach_theta) attach_points = [ (15, 4), (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s), ] root_profile, middle_profile, tip_profile = wing_root_profiles( conn_thickness=conn_thickness, conn_height=conn_height, wall_thickness=8, ) middle_profile = middle_profile.located(Cq.Location( (-40, 0, -40), (0, 1, 0), 30 )) antetip_profile = tip_profile.located(Cq.Location( (-95, 0, -75), (0, 1, 0), 60 )) tip_profile = tip_profile.located(Cq.Location( tip_centre, (0, 1, 0), 90 )) profiles = [ root_profile, middle_profile, antetip_profile, tip_profile, ] result = None for p1, p2 in zip(profiles[:-1], profiles[1:]): seg = ( Cq.Workplane('XY') .add(p1) .toPending() .workplane() # This call is necessary .add(p2) .toPending() .loft() ) if result: result = result.union(seg, tol=union_tol) else: result = seg result = ( result # Create connector holes .copyWorkplane( Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0))) ) .pushPoints(attach_points) .hole(shoulder_attach_diam) ) # Generate attach point tags for sign in [False, True]: y = conn_height / 2 - wall_thickness side = "bottom" if sign else "top" y = -y if sign else y plane = ( result # Create connector holes .copyWorkplane( Cq.Workplane(side, origin=tip_centre + Cq.Vector((0, y, 0))) ) ) if side == "bottom": side = "bot" for i, (px, py) in enumerate(attach_points): tag = f"conn_{side}{i}" plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z") result.faces("X").tag("conn") j = ( joint.generate(is_mated=True) .faces(" list[Tuple[float, float]]: """ Generate outer wing shape spline """ def profile(self) -> Cq.Sketch: sketch = ( Cq.Sketch() .segment((-self.root_width, 0), (0, 0)) .spline([ (0, 0), (-30.0, 80.0), (self.tip_x, self.tip_y) ]) .segment( (self.tip_x, self.tip_y), (self.tip_x, self.tip_y - self.shoulder_width) ) .segment( (self.tip_x, self.tip_y - self.shoulder_width), (-self.root_width, 0) ) .assemble() ) return sketch def 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 """ length = self.height width = 10.0 h = self.spacer_thickness result = ( Cq.Workplane('XY') .sketch() .rect(length, width) .finalize() .extrude(h) ) # Tag the mating surfaces to be glued result.faces("X").workplane().tagPlane("right") # Tag the directrix result.faces(">Z").tag("dir") return result def surface(self, top: bool = False) -> Cq.Workplane: tags = [ ("shoulder", (self.tip_x, self.tip_y + 30), 0), ("base", (-self.root_width, 0), 90), ] return nhf.utils.extrude_with_markers( self.profile(), self.panel_thickness, tags, reverse=not top, ) def assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() .add(self.surface(top=True), name="bot") .add(self.surface(top=False), name="top") .constrain("bot@faces@>Z", "top@faces@ self.ring_radius_inner 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) 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 @property def ring_radius(self) -> float: dx = self.ring_x dy = self.ring_y return (dx * dx + dy * dy) ** 0.5 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 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_height), tag="shoulder") .arc( (0, self.shoulder_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") .arc( (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 def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow """ abscissa = 200 return [ (0, -abscissa), (self.elbow_x, self.elbow_y), (self.elbow_top_x, self.elbow_top_y), (0, abscissa) ] def _mask_wrist(self) -> list[Tuple[float, float]]: abscissa = 200 return [ (0, -abscissa), (self.wrist_x, -abscissa), (self.wrist_x, self.wrist_y), (self.wrist_top_x, self.wrist_top_y), (0, abscissa), ] def profile_s1(self) -> Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_elbow(), mode='i') ) return profile def surface_s1(self, thickness: float = 25.4/16, shoulder_mount_inset: float = 20, shoulder_joint_child_height: float = 80, elbow_mount_inset: float = 20, elbow_joint_parent_height: float = 60, front: bool = True) -> Cq.Workplane: assert shoulder_joint_child_height < self.shoulder_height assert elbow_joint_parent_height < self.elbow_height h = (self.shoulder_height - shoulder_joint_child_height) / 2 tags_shoulder = [ ("shoulder_bot", (shoulder_mount_inset, h), 90), ("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270), ] h = (self.elbow_height - elbow_joint_parent_height) / 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_joint_parent_height), self.elbow_angle + 270), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) def profile_s2(self) -> 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 = 20, elbow_joint_child_height: float = 80, wrist_mount_inset: float = 20, wrist_joint_parent_height: float = 60, front: bool = True) -> Cq.Workplane: assert elbow_joint_child_height < self.elbow_height h = (self.elbow_height - elbow_joint_child_height) / 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_joint_child_height), self.elbow_angle - 90), ] h = (self.wrist_height - wrist_joint_parent_height) / 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_joint_parent_height), self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) def profile_s3(self) -> Cq.Sketch: profile = ( self.profile() .reset() .polygon(self._mask_wrist(), mode='s') ) return profile def surface_s3(self, thickness: float = 25.4/16, wrist_mount_inset: float = 20, wrist_joint_child_height: float = 80, front: bool = True) -> Cq.Workplane: assert wrist_joint_child_height < self.wrist_height h = (self.wrist_height - wrist_joint_child_height) / 2 tags = [ ("wrist_bot", self.elbow_to_abs(wrist_mount_inset, h), self.elbow_angle + 90), ("wrist_top", self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), self.elbow_angle - 90), ] profile = self.profile_s3() return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) def wing_r1s1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in the positive x axis. """ w = 270 # Depression of the wing middle, measured h = 0 # spline curve easing extension theta = math.radians(30) c_th, s_th = math.cos(theta), math.sin(theta) bend = 30 ext = 40 ext_dh = -5 assert ext * 2 < w factor = 0.7 result = ( Cq.Sketch() .segment((0, 0), (0, self.shoulder_height)) .spline([ (0, self.shoulder_height), ((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend), (w - s_th * self.elbow_height, self.elbow_height * c_th - h), ]) .segment( (w - s_th * self.elbow_height, self.elbow_height * c_th -h), (w, -h), ) .spline([ (0, 0), (w / 2, -h / 2 - bend), (w, -h), ]) .assemble() ) return result