diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2b0f7a2..c33deda 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -55,7 +55,7 @@ class WingProfile(Model): movement_angle=55, ), hole_diam=4.0, - angle_neutral=15.0, + angle_neutral=30.0, actuator=LINEAR_ACTUATOR_50, flexor_offset_angle=0, flip=False, @@ -92,7 +92,9 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float - elbow_rotate: float = -5.0 + elbow_rotate: float = 10.0 + elbow_joint_overlap_median: float = 0.3 + wrist_joint_overlap_median: float = 0.5 wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) elbow_axle_pos: float = 0.5 @@ -428,6 +430,9 @@ class WingProfile(Model): """ Generates profile from shoulder and above. Subclass should implement """ + @target(name="profile-s2-bridge", kind=TargetKind.DXF) + def profile_s2_bridge(self) -> Optional[Cq.Sketch]: + return None @target(name="profile-s3-extra", kind=TargetKind.DXF) def profile_s3_extra(self) -> Optional[Cq.Sketch]: """ @@ -460,73 +465,44 @@ class WingProfile(Model): for p in points ]) ) - def _child_joint_extension_profile( - self, - axle_loc: Cq.Location, - radius: float, - angle_span: float, - bot: bool = False) -> Cq.Sketch: - """ - Creates a sector profile which accomodates extension - """ - sign = -1 if bot else 1 - axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) - loc_h = Cq.Location.from2d(radius, 0) - start = axle_loc * loc_h - mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h - end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h - return ( - Cq.Sketch() - .segment( - axle_loc.to2d_pos(), - start.to2d_pos(), - ) - .arc( - start.to2d_pos(), - mid.to2d_pos(), - end.to2d_pos(), - ) - .segment( - end.to2d_pos(), - axle_loc.to2d_pos(), - ) - .assemble() - ) - def _parent_joint_extension_profile( + def _joint_extension_cut_polygon( self, - loc_axle: Cq.Location, loc_bot: Cq.Location, loc_top: Cq.Location, + height: float, angle_span: float, - bot: bool = True + axle_pos: float, + bot: bool = True, + child: bool = False, + overestimate: float = 1.2, + median: float = 0.5, ) -> Cq.Sketch: """ - Generates a sector-like profile on the child side of a panel to - accomodate for joint rotation + A cut polygon to accomodate for joint extensions """ - sign = -1 if bot else 1 - + loc_ext = loc_bot if bot else loc_top loc_tip = loc_top if bot else loc_bot - loc_arc_right = loc_bot if bot else loc_top - loc_rel_arc_right = loc_axle.inverse * loc_arc_right - loc_arc_left = loc_axle * Cq.Location.rot2d(sign * angle_span) * loc_rel_arc_right - loc_arc_middle = loc_axle * Cq.Location.rot2d(sign * angle_span / 2) * loc_rel_arc_right + theta = math.radians(angle_span * (median if child else 1 - median)) + y_sign = -1 if bot else 1 + sign = -1 if child else 1 + dh = axle_pos * height * (overestimate - 1) + loc_left = loc_ext * Cq.Location.from2d(0, y_sign * dh) + loc_right = loc_left * Cq.Location.from2d(sign * height * overestimate * axle_pos * math.tan(theta), 0) return ( Cq.Sketch() .segment( loc_tip.to2d_pos(), - loc_arc_right.to2d_pos(), - ) - .arc( - loc_arc_right.to2d_pos(), - loc_arc_middle.to2d_pos(), - loc_arc_left.to2d_pos(), + loc_left.to2d_pos(), ) .segment( + loc_left.to2d_pos(), + loc_right.to2d_pos(), + ) + .segment( + loc_right.to2d_pos(), loc_tip.to2d_pos(), - loc_arc_left.to2d_pos(), ) .assemble() ) @@ -597,10 +573,22 @@ class WingProfile(Model): @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: + cut_poly = self._joint_extension_cut_polygon( + loc_bot=self.elbow_bot_loc, + loc_top=self.elbow_top_loc, + height=self.elbow_height, + angle_span=self.elbow_joint.motion_span, + axle_pos=self.elbow_axle_pos, + bot=not self.elbow_joint.flip, + median=self.elbow_joint_overlap_median, + child=False, + ).reset().polygon(self._mask_elbow(), mode='a') profile = ( self.profile() .reset() - .polygon(self._mask_elbow(), mode='i') + .push([self.elbow_axle_loc.to2d_pos()]) + .each(lambda _: cut_poly, mode='i') + #.polygon(self._mask_elbow(), mode='i') ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: @@ -686,21 +674,47 @@ class WingProfile(Model): @target(name="profile-s2", kind=TargetKind.DXF) def profile_s2(self) -> Cq.Sketch: + # Calculates `(profile - (E - JE)) * (W + JW)` + cut_elbow = ( + Cq.Sketch() + .polygon(self._mask_elbow()) + .reset() + .boolean(self._joint_extension_cut_polygon( + loc_bot=self.elbow_bot_loc, + loc_top=self.elbow_top_loc, + height=self.elbow_height, + angle_span=self.elbow_joint.motion_span, + axle_pos=self.elbow_axle_pos, + bot=not self.elbow_joint.flip, + median=self.elbow_joint_overlap_median, + child=True, + ), mode='s') + ) + cut_wrist = ( + Cq.Sketch() + .polygon(self._mask_wrist()) + ) + if self.flip: + poly = self._joint_extension_cut_polygon( + loc_bot=self.wrist_bot_loc, + loc_top=self.wrist_top_loc, + height=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + axle_pos=self.wrist_axle_pos, + bot=not self.wrist_joint.flip, + median=self.wrist_joint_overlap_median, + child=False, + ) + cut_wrist = ( + cut_wrist + .reset() + .boolean(poly, mode='a') + ) profile = ( self.profile() .reset() - .polygon(self._mask_elbow(), mode='s') - .reset() - .polygon(self._mask_wrist(), mode='i') - .reset() - .push([self.elbow_axle_loc]) - .each(lambda loc: self._parent_joint_extension_profile( - loc, - self.elbow_bot_loc, - self.elbow_top_loc, - self.elbow_joint.motion_span, - bot=not self.flip, - ), mode='a') + .boolean(cut_elbow, mode='s') + .boolean(cut_wrist, mode='i') ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: @@ -724,33 +738,10 @@ class WingProfile(Model): profile = self.profile_s2() tags = tags_elbow + tags_wrist return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) - @target(name="profile-s2-bridge", kind=TargetKind.DXF) - def profile_s2_bridge(self) -> Cq.Workplane: - # FIXME: Leave some margin here so we can glue the panels - - # Generates the extension profile, which is required on both sides - profile = self._child_joint_extension_profile( - axle_loc=self.wrist_axle_loc, - radius=self.wrist_height * (0.5 if self.flip else 1), - angle_span=self.wrist_joint.motion_span, - bot=self.flip, - ) - # Generates the contraction (cut) profile. only required on the left - if self.flip: - extra = ( - self.profile() - .reset() - .push([self.wrist_axle_loc]) - .each(self._wrist_joint_retract_cut_polygon, mode='i') - ) - profile = ( - profile - .push([self.wrist_axle_loc]) - .each(lambda _: extra, mode='a') - ) - return profile - def surface_s2_bridge(self, front: bool = True) -> Cq.Workplane: + def surface_s2_bridge(self, front: bool = True) -> Optional[Cq.Workplane]: profile = self.profile_s2_bridge() + if profile is None: + return None loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * @@ -796,15 +787,25 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: + cut_wrist = ( + Cq.Sketch() + .polygon(self._mask_wrist()) + ) + if self.flip: + poly = self._joint_extension_cut_polygon( + loc_bot=self.wrist_bot_loc, + loc_top=self.wrist_top_loc, + height=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + axle_pos=self.wrist_axle_pos, + bot=not self.wrist_joint.flip, + median=self.wrist_joint_overlap_median, + child=True, + ) + cut_wrist = ( + cut_wrist + .boolean(poly, mode='s') + ) profile = ( self.profile() - .reset() - .polygon(self._mask_wrist(), mode='s') + .boolean(cut_wrist, mode='s') ) return profile def surface_s3(self, @@ -1029,7 +1048,6 @@ class WingR(WingProfile): # Underapproximate the wrist tangent angle to leave no gaps on the blade blade_wrist_approx_tangent_angle: float = 40.0 - blade_overlap_arrow_height: float = 5.0 # Some overlap needed to glue the two sides blade_overlap_angle: float = -1 blade_hole_angle: float = 3 @@ -1116,6 +1134,74 @@ class WingR(WingProfile): ) return result + def _child_joint_extension_profile( + self, + axle_loc: Cq.Location, + radius: float, + angle_span: float, + bot: bool = False) -> Cq.Sketch: + """ + Creates a sector profile which accomodates extension + """ + # leave some margin for gluing + margin = 5 + sign = -1 if bot else 1 + axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) + loc_h = Cq.Location.from2d(radius, 0) + loc_offset = axle_loc * Cq.Location.from2d(0, margin) + start = axle_loc * loc_h + mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h + end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h + return ( + Cq.Sketch() + .segment( + loc_offset.to2d_pos(), + start.to2d_pos(), + ) + .arc( + start.to2d_pos(), + mid.to2d_pos(), + end.to2d_pos(), + ) + .segment( + end.to2d_pos(), + axle_loc.to2d_pos(), + ) + .segment( + axle_loc.to2d_pos(), + loc_offset.to2d_pos(), + ) + .assemble() + ) + + @target(name="profile-s2-bridge", kind=TargetKind.DXF) + def profile_s2_bridge(self) -> Cq.Sketch: + """ + This extension profile is required to accomodate the awkward shaped + joint next to the scythe + """ + # Generates the extension profile, which is required on both sides + profile = self._child_joint_extension_profile( + axle_loc=self.wrist_axle_loc, + radius=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + bot=self.flip, + ) + # Generates the contraction (cut) profile. only required on the left + if self.flip: + extra = ( + self.profile() + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='i') + ) + profile = ( + profile + .push([self.wrist_axle_loc]) + .each(lambda _: extra, mode='a') + ) + return profile + def profile_s3_extra(self) -> Cq.Sketch: """ Implements the blade part on Nue's wing @@ -1123,7 +1209,7 @@ class WingR(WingProfile): left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1) hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle) right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle) - h_loc = Cq.Location.from2d(0, self.arrow_height + self.blade_overlap_arrow_height) + h_loc = Cq.Location.from2d(0, self.arrow_height) # Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?) theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle) @@ -1202,6 +1288,9 @@ class WingL(WingProfile): elbow_axle_pos: float = 0.4 wrist_axle_pos: float = 0.5 + elbow_joint_overlap_median: float = 0.5 + wrist_joint_overlap_median: float = 0.5 + def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) diff --git a/nhf/utils.py b/nhf/utils.py index 1d2c796..13469fd 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -100,6 +100,15 @@ def flip_y(self: Cq.Location) -> Cq.Location: return Cq.Location.from2d(x, -y, -a) Cq.Location.flip_y = flip_y +def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch: + return ( + self + .reset() + .push([(0, 0)]) + .each(lambda _: obj, **kwargs) + ) +Cq.Sketch.boolean = boolean + ### Tags def tagPoint(self, tag: str):