diff --git a/nhf/build.py b/nhf/build.py index 3db9c36..106577c 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -69,8 +69,13 @@ class Target: else: return None - def write_to(self, obj, path: str): + def write_to(self, obj, path: str) -> bool: + """ + Returns false if target is `None` + """ x = self._method(obj) + if x is None: + return False if self.kind == TargetKind.STL: assert isinstance(x, Union[ Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) @@ -92,6 +97,7 @@ class Target: Cq.exporters.exportDXF(x, path, **self.kwargs) else: assert False, f"Invalid kind: {self.kind}" + return True @classmethod def methods(cls, subject): @@ -257,8 +263,9 @@ class Model: """ total = 0 for t in Target.methods(self).values(): - t(self) - total += 1 + result = t(self) + if result: + total += 1 for t in Assembly.methods(self).values(): t.check(self) total += 1 @@ -288,8 +295,8 @@ class Model: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") try: - t.write_to(self, str(output_file)) - if verbose >= 1: + flag = t.write_to(self, str(output_file)) + if flag and verbose >= 1: print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") except Exception as e: print(f"{Fore.RED}Failed to build{Style.RESET_ALL} {output_file}: {e}") diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index c368d60..5a79461 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -16,7 +16,7 @@ TOL = 1e-6 # uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP HS_JOINT_KNOB = ThreaddedKnob( - mass=float('nan'), + mass=0.0, # FIXME: Measure diam_thread=12.0, height_thread=30.0, diam_knob=50.0, @@ -36,11 +36,11 @@ HS_JOINT_HEX_NUT = HexNut( ) SHOULDER_AXIS_BOLT = FlatHeadBolt( # FIXME: measure + mass=0.0, diam_head=10.0, height_head=3.0, diam_thread=6.0, height_thread=20.0, - mass=float('nan'), ) # Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional # Springs Repair Maintenance Spring diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 27c4c6e..82ed569 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,7 +62,7 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, - angle_neutral=30.0, + angle_neutral=-20.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -80,10 +80,10 @@ class WingProfile(Model): wrist_bot_loc: Cq.Location wrist_height: float elbow_rotate: float = -5.0 - wrist_rotate: float = 30.0 + wrist_rotate: float = -20.0 # False for the right side, True for the left side - flip: bool = False + flip: bool def __post_init__(self): super().__init__(name=self.name) @@ -91,7 +91,10 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) - self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + if self.flip: + self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + else: + self.wrist_axle_loc = self.wrist_bot_loc assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) @@ -329,6 +332,12 @@ class WingProfile(Model): """ Generates profile from shoulder and above. Subclass should implement """ + @target(name="profile-s3-extra", kind=TargetKind.DXF) + def profile_s3_extra(self) -> Optional[Cq.Sketch]: + """ + Extra element to be glued on s3. Not needed for left side + """ + return None def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: """ @@ -351,10 +360,15 @@ class WingProfile(Model): for p in points ]) ) - def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: + def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Optional[Cq.Sketch]: """ Creates a cutting polygon for removing the contraction part of a joint """ + if not self.flip: + """ + No cutting needed on RHS + """ + return None theta = math.radians(self.wrist_joint.motion_span) dx = self.wrist_height * math.tan(theta) dy = self.wrist_height @@ -521,10 +535,14 @@ class WingProfile(Model): .reset() .push([self.elbow_axle_loc]) .each(self._elbow_joint_retract_cut_polygon, mode='s') - .reset() - .push([self.wrist_axle_loc]) - .each(self._wrist_joint_retract_cut_polygon, mode='s') ) + if self.flip: + profile = ( + profile + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='s') + ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 @@ -595,7 +613,6 @@ class WingProfile(Model): return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: - h = self.wrist_height / 2 loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * @@ -605,6 +622,19 @@ class WingProfile(Model): ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def surface_s3_extra(self, + front: bool = True) -> Optional[Cq.Workplane]: + profile = self.profile_s3_extra() + if profile is None: + return None + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) + tags = [ + ("wrist_bot", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, self.wrist_h2)), + ("wrist_top", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, -self.wrist_h2)), + ] + return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: return self.spacer_of_joint( @@ -624,6 +654,18 @@ class WingProfile(Model): .constrain("front@faces@>Z", "back@faces@ self.ring_radius_inner + assert 0 > self.blade_overlap_angle > self.arrow_angle + assert 0 < self.blade_hole_angle < self.blade_angle + assert self.blade_wrist_approx_tangent_angle <= self.wrist_bot_loc.to2d_rot() + @property def ring_radius(self) -> float: (dx, dy), _ = self.ring_rel_loc.to2d() @@ -829,6 +887,43 @@ class WingR(WingProfile): ) return result + def profile_s3_extra(self) -> Cq.Sketch: + """ + Implements the blade part on Nue's wing + """ + 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) + + # Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?) + theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle) + theta_b = math.radians(self.blade_angle) + h_blade = math.sin(theta_wp) / math.sin(math.pi - theta_b - theta_wp) * self.arrow_height + h_blade_loc = Cq.Location.from2d(0, h_blade) + return ( + Cq.Sketch() + .segment( + self.arrow_bot_loc.to2d_pos(), + (left_bot_loc * h_loc).to2d_pos(), + ) + .segment( + (self.arrow_bot_loc * h_loc).to2d_pos(), + ) + .segment( + (right_bot_loc * h_blade_loc).to2d_pos(), + ) + .close() + .assemble() + .reset() + .push([ + (hole_bot_loc * Cq.Location.from2d(0, h)).to2d_pos() + for h in self.blade_hole_heights + ]) + .circle(self.blade_hole_diam / 2, mode='s') + ) + + def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 elbow_x, _ = self.elbow_bot_loc.to2d_pos() diff --git a/nhf/utils.py b/nhf/utils.py index e999e85..3399fce 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -72,7 +72,7 @@ def to2d_pos(self: Cq.Location) -> Tuple[float, float]: return x, y Cq.Location.to2d_pos = to2d_pos -def to2d_rot(self: Cq.Location) -> Tuple[float, float]: +def to2d_rot(self: Cq.Location) -> float: """ Returns position and angle """