diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index ea4947f..70c5d03 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -8,7 +8,9 @@ from nhf.parts.springs import TorsionSpring from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes -from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator +from nhf.touhou.houjuu_nue.electronics import ( + Flexor, LinearActuator, LINEAR_ACTUATOR_21, +) import nhf.geometry import nhf.utils @@ -286,17 +288,18 @@ class ShoulderJoint(Model): # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. - child_guard_ext: float = 30.0 + child_guard_ext: float = 20.0 child_guard_width: float = 25.0 # guard length measured from axle - child_lip_length: float = 40.0 + child_lip_ext: float = 50.0 child_lip_width: float = 20.0 + child_lip_height: float = 40.0 + child_lip_thickness: float = 5.0 child_conn_hole_diam: float = 4.0 # Measured from centre of axle - child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30]) + child_conn_hole_pos: list[float] = field(default_factory=lambda: [-15, -5, 5, 15]) child_core_thickness: float = 3.0 - # Rotates the torsion joint to avoid collisions or for some other purpose axis_rotate_bot: float = 90 axis_rotate_top: float = 0 @@ -311,10 +314,16 @@ class ShoulderJoint(Model): spool_height: float = 5.0 spool_groove_inset: float = 2.0 + flip: bool = False + actuator: LinearActuator = LINEAR_ACTUATOR_21 + def __post_init__(self): assert self.parent_lip_length * 2 < self.height + assert self.child_guard_ext > self.torsion_joint.radius_rider assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness assert self.spool_base_height > self.spool_groove_depth + assert self.child_lip_height < self.height + assert self.draft_length <= self.actuator.stroke_length @property def radius(self): @@ -327,6 +336,13 @@ class ShoulderJoint(Model): """ return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection) + @property + def draft_height(self): + """ + Position of the middle of the spool measured from the middle + """ + return self.height / 2 - self.torsion_joint.total_height - self.spool_base_height / 2 + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle @@ -335,10 +351,15 @@ class ShoulderJoint(Model): sign = 1 if left else -1 loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90) return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir + def child_lip_loc(self) -> Cq.Location: + """ + 2d location to middle of lip + """ + return Cq.Location.from2d(self.child_lip_ext - self.child_guard_ext, 0, 180) @property def _max_contraction_angle(self) -> float: - return self.angle_max_deflection + self.angle_neutral + return 180 - self.angle_max_deflection + self.angle_neutral def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: """ @@ -346,7 +367,8 @@ class ShoulderJoint(Model): """ aspect = self.child_guard_width / self.parent_arm_width theta = math.radians(self._max_contraction_angle) - theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) + #theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) + theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect) angle = math.degrees(theta_p) assert 0 <= angle <= 90 # outer radius of the cut, overestimated @@ -431,10 +453,7 @@ class ShoulderJoint(Model): @target(name="parent-bot") def parent_bot(self) -> Cq.Assembly: - result = ( - self.parent(top=False) - ) - return result + return self.parent(top=False) @target(name="parent-top") def parent_top(self) -> Cq.Assembly: return self.parent(top=True) @@ -485,18 +504,15 @@ class ShoulderJoint(Model): """ joint = self.torsion_joint - assert all(r < self.child_lip_length for r in self.child_conn_hole_pos) # Half of the height of the bridging cylinder dh = self.height / 2 - joint.total_height core_start_angle = 30 - core_end_angle1 = 90 - core_end_angle2 = 180 radius_core_inner = joint.radius_rider - self.child_core_thickness core_profile1 = ( Cq.Sketch() - .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) + .arc((0, 0), joint.radius_rider, core_start_angle, self.angle_max_deflection) .segment((0, 0)) .close() .assemble() @@ -504,12 +520,28 @@ class ShoulderJoint(Model): ) core_profile2 = ( Cq.Sketch() - .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) + .arc((0, 0), joint.radius_rider, -core_start_angle, -(90 - self.angle_neutral)) .segment((0, 0)) .close() .assemble() .circle(radius_core_inner, mode='s') ) + lip_extension = ( + Cq.Solid.makeBox( + length=self.child_lip_ext - self.child_guard_ext, + width=self.child_lip_width, + height=self.child_lip_height, + ).cut(Cq.Solid.makeBox( + length=self.child_lip_ext - self.child_guard_ext, + width=self.child_lip_width - self.child_lip_thickness * 2, + height=self.child_lip_height, + ).located(Cq.Location((0, self.child_lip_thickness, 0)))) + .located(Cq.Location(( + self.child_guard_ext, + -self.child_lip_width / 2, + -self.child_lip_height / 2, + ))) + ) core_guard = ( Cq.Workplane('XY') .box( @@ -518,6 +550,14 @@ class ShoulderJoint(Model): height=self.height, centered=(False, True, True), ) + #.copyWorkplane(Cq.Workplane('XY')) + #.box( + # length=self.child_lip_ext, + # width=self.child_guard_width, + # height=self.child_lip_height, + # combine=True, + # centered=(False, True, True), + #) .copyWorkplane(Cq.Workplane('XY')) .cylinder( radius=self.radius, @@ -527,12 +567,13 @@ class ShoulderJoint(Model): ) .copyWorkplane(Cq.Workplane('XY')) .box( - length=self.child_guard_ext, + length=self.child_lip_ext, width=self.child_lip_width, height=self.height - self.torsion_joint.total_height * 2, combine='cut', centered=(False, True, True), ) + .union(lip_extension) .cut(self._contraction_cut_geometry(parent=False)) ) core = ( @@ -548,13 +589,17 @@ class ShoulderJoint(Model): .union(core_guard) ) assert self.child_lip_width / 2 <= joint.radius_rider - lip_thickness = joint.rider_disk_height - lip = box_with_centre_holes( - length=self.child_lip_length, + sign = 1 if self.flip else -1 + holes = [Hole(x=sign * x) for x in self.child_conn_hole_pos] + lip_obj = MountingBox( + length=self.child_lip_height, width=self.child_lip_width, - height=lip_thickness, - hole_loc=self.child_conn_hole_pos, + thickness=self.child_lip_thickness, + holes=holes, hole_diam=self.child_conn_hole_diam, + centred=(True, True), + generate_side_tags=False, + generate_reverse_tags=False, ) theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) @@ -562,6 +607,7 @@ class ShoulderJoint(Model): loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) spool_dz = self.height / 2 - self.torsion_joint.total_height spool_angle = 180 + self.angle_neutral + loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location() result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) @@ -569,12 +615,10 @@ class ShoulderJoint(Model): loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta)) .add(joint.rider(rider_slot_begin=180), name="rider_bot", loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) - .add(lip, name="lip_top", - loc=Cq.Location((self.child_guard_ext, 0, dh))) - .add(lip, name="lip_bot", - loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate) + .add(lip_obj.generate(), name="lip", + loc=Cq.Location((self.child_lip_ext - self.child_lip_thickness,0,0), (0,1,0), 90)) .add(self._spool(), name="spool", - loc=Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) + loc=loc_spool_flip * Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) ) return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 0176e0e..22efbbc 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -337,6 +337,19 @@ class WingProfile(Model): flip_y=self.flip, generate_reverse_tags=True, ) + @submodel(name="spacer-s0-shoulder-act") + def spacer_s0_shoulder_act(self) -> MountingBox: + dx = self.shoulder_joint.draft_height + return MountingBox( + holes=[Hole(x=dx), Hole(x=-dx)], + hole_diam=self.shoulder_joint.actuator.back_hole_diam, + length=self.root_height, + width=10.0, + centred=(True, True), + thickness=self.spacer_thickness, + flip_y=self.flip, + generate_reverse_tags=True, + ) def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 @@ -352,6 +365,8 @@ class WingProfile(Model): self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)), ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), + ("shoulder_act", + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(150, -40)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-45, 75, 64)), ] @@ -393,6 +408,7 @@ class WingProfile(Model): for o, tag in [ (self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"), (self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"), + (self.spacer_s0_shoulder_act().generate(), "shoulder_act"), (self.spacer_s0_base().generate(), "base"), (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: @@ -599,13 +615,11 @@ class WingProfile(Model): ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: - shoulder_h = self.shoulder_joint.child_height - h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ - ("shoulder_bot", Cq.Location.from2d(0, h, 90)), - ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), + ("shoulder", + Cq.Location((0, self.shoulder_height / 2, 0)) * + self.shoulder_joint.child_lip_loc()), ] - h = self.elbow_height / 2 rot_elbow = Cq.Location.rot2d(self.elbow_rotate) loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() tags_elbow = [ @@ -622,16 +636,20 @@ class WingProfile(Model): profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: + sign = -1 if self.flip else 1 holes = [ - Hole(x) + Hole(x=sign * x) for x in self.shoulder_joint.child_conn_hole_pos ] return MountingBox( - length=50.0, # FIXME: magic + length=self.shoulder_joint.child_lip_height, width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, + centred=(True, True), hole_diam=self.shoulder_joint.child_conn_hole_diam, + centre_left_right_tags=True, + centre_bot_top_tags=True, ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: @@ -664,8 +682,7 @@ class WingProfile(Model): param=self.s1_thickness) ) for o, t in [ - (self.spacer_s1_shoulder(), "shoulder_bot"), - (self.spacer_s1_shoulder(), "shoulder_top"), + (self.spacer_s1_shoulder(), "shoulder"), (self.spacer_s1_elbow(), "elbow_top"), (self.spacer_s1_elbow(), "elbow_bot"), (self.spacer_s1_elbow_act(), "elbow_act"), @@ -988,13 +1005,8 @@ class WingProfile(Model): 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", f"shoulder/child/lip_{tag_top}?conn0", "Plane") - .constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane") - .constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane") - .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") - ) + for i in range(len(self.shoulder_joint.child_conn_hole_pos)): + result.constrain(f"s1/shoulder?conn{i}", f"shoulder/child/lip?conn{i}", "Plane") if "elbow" in parts: angle = self.elbow_joint.motion_span * elbow_wrist_deflection result.add(self.elbow_joint.assembly( @@ -1327,6 +1339,7 @@ class WingL(WingProfile): self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_rotate = -self.wrist_joint.angle_neutral self.wrist_joint.flip = False + self.shoulder_joint.flip = True super().__post_init__()