diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index dfccb5a..1eeb1e8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -548,7 +548,7 @@ class Parameters(Model): @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: - return self.wing_profile.wing_r1_profile() + return self.wing_profile.profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: profile = self.wing_r1s1_profile() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b6551b8..b7dcd7c 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -3,7 +3,9 @@ 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 @@ -235,19 +237,19 @@ class WingProfile: shoulder_height: float = 100 - elbow_height: float = 120 - elbow_x: float = 270 - elbow_y: float = 10 - # Angle of elbow w.r.t. y axis - elbow_angle: float = -20 + elbow_height: float = 100 + elbow_x: float = 240 + elbow_y: float = 30 + # Tilt of elbow w.r.t. shoulder + elbow_angle: float = 20 wrist_height: float = 70 # Bottom point of the wrist wrist_x: float = 400 wrist_y: float = 200 - # Angle of wrist w.r.t. y axis. should be negative - wrist_angle: float = -40 + # 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 @@ -261,21 +263,45 @@ class WingProfile: def __post_init__(self): assert self.ring_radius > 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 wing_r1_profile(self) -> Cq.Sketch: - wrist_theta = math.radians(self.wrist_angle) - wrist_s = math.sin(wrist_theta) - wrist_c = math.cos(wrist_theta) - wrist_top_x = self.wrist_x + self.wrist_height * wrist_s - wrist_top_y = self.wrist_y + self.wrist_height * wrist_c - elbow_theta = math.radians(self.elbow_angle) - elbow_top_x = self.elbow_x + self.elbow_height * math.sin(elbow_theta) - elbow_top_y = self.elbow_y + self.elbow_height * math.cos(elbow_theta) + 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 + print(f"c={self.elbow_c}, s={self.elbow_s}, x={elbow_x}, y={elbow_y}") + 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( @@ -284,8 +310,8 @@ class WingProfile: tag="shoulder") .arc( (0, self.shoulder_height), - (elbow_top_x, elbow_top_y), - (wrist_top_x, wrist_top_y), + (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), @@ -297,39 +323,108 @@ class WingProfile: (self.wrist_x, self.wrist_y), tag="s1_bot") ) - arrow_theta = math.radians(self.arrow_angle) - arrow_x = self.wrist_x - self.arrow_height * wrist_s - arrow_y = self.wrist_y - self.arrow_height * wrist_c - arrow_tip_x = arrow_x + (self.arrow_height + self.wrist_height) * math.sin(arrow_theta + wrist_theta) - arrow_tip_y = arrow_y + (self.arrow_height + self.wrist_height) * math.cos(arrow_theta + wrist_theta) result = ( result .segment( (self.wrist_x, self.wrist_y), - (arrow_x, arrow_y) + (self.arrow_x, self.arrow_y) ) .segment( - (arrow_x, arrow_y), - (arrow_tip_x, arrow_tip_y) + (self.arrow_x, self.arrow_y), + (self.arrow_tip_x, self.arrow_tip_y) ) .segment( - (arrow_tip_x, arrow_tip_y), - (wrist_top_x, wrist_top_y) + (self.arrow_tip_x, self.arrow_tip_y), + (self.wrist_top_x, self.wrist_top_y) ) ) # Carve out the ring result = result.assemble() - ring_x = wrist_top_x + wrist_c * self.ring_x + wrist_s * self.ring_y - ring_y = wrist_top_y - wrist_s * self.ring_x + wrist_c * self.ring_y result = ( result - .push([(ring_x, ring_y)]) + .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 - self.wrist_s * abscissa, + self.wrist_y - self.wrist_c * abscissa), + (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_wrist(), mode='i') + .reset() + .polygon(self._mask_elbow(), mode='s') + ) + return profile + def profile_s3(self) -> Cq.Sketch: + profile = ( + self.profile() + .reset() + .polygon(self._mask_wrist(), mode='s') + ) + return profile + + def wing_r1s1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in diff --git a/nhf/utils.py b/nhf/utils.py index f375c63..6d1dc8b 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -5,6 +5,7 @@ Adds the functions to `Cq.Workplane`: 1. `tagPoint` 2. `tagPlane` """ +import math import cadquery as Cq from typing import Union, Tuple @@ -50,3 +51,31 @@ def tagPlane(self, tag: str, self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag) Cq.Workplane.tagPlane = tagPlane + +def extrude_with_markers(sketch: Cq.Sketch, + thickness: float, + tags: list[Tuple[str, Tuple[float, float], float]], + reverse: bool = False): + """ + Extrudes a sketch and place tags on the sketch for mating. + + Each tag is of the format `(name, (x, y), angle)`, where the angle is + specifies in degrees counterclockwise from +X. Two marks are generated for + each `name`, "{name}" for the location (with normal) and "{name}_dir" for + the directrix specified by the angle. + + This simulates a process of laser cutting and bonding (for wood and acrylic) + """ + result = ( + Cq.Workplane('XY') + .placeSketch(sketch) + .extrude(thickness) + ) + plane = result.faces("Z").workplane() + sign = -1 if reverse else 1 + for tag, (px, py), angle in tag: + theta = sign * math.radians(angle) + direction = (math.cos(theta), math.sin(theta), 0) + plane.moveTo(px, sign * py).tagPlane(tag) + plane.moveTo(px, sign * py).tagPlane(f"{tag}_dir", direction) + return result