From 4c5985fa08a6b18da62bab37952af9182402ab74 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 14:03:01 -0700 Subject: [PATCH] feat: Bent elbow joint --- nhf/touhou/houjuu_nue/joints.py | 61 ++++++++++++++++++++++++--------- nhf/touhou/houjuu_nue/wing.py | 56 +++++++++++++++--------------- nhf/utils.py | 9 +++++ 3 files changed, 80 insertions(+), 46 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5fa9a6d..78cd819 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -313,6 +313,9 @@ class Beam: class DiskJoint(Model): """ Sandwiched disk joint for the wrist and elbow + + We embed a spring inside the joint, with one leg in the disk and one leg in + the housing. This provides torsion resistance. """ spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( radius=9 / 2, @@ -328,14 +331,15 @@ class DiskJoint(Model): housing_thickness: float = 4.0 disk_thickness: float = 7.0 - # Gap between disk and the housing - #disk_thickness_gap: float = 0.1 + + # Amount by which the wall carves in + wall_inset: float = 2.0 + # Height of the spring hole; if you make it too short the spring can't enter + spring_tail_hole_height: float = 2.0 # Spring angle at 0 degrees of movement - spring_angle_at_0: float = 60.0 - spring_slot_offset: float = 15.0 - - wall_inset: float = 2.0 + spring_angle_at_0: float = 90.0 + spring_slot_offset: float = 5.0 # Angular span of movement movement_angle: float = 120.0 @@ -348,10 +352,11 @@ class DiskJoint(Model): def __post_init__(self): super().__init__(name="disk-joint") - assert self.housing_thickness > self.wall_inset - assert self.radius_housing > self.radius_disk - assert self.radius_disk > self.radius_axle + assert self.radius_housing > self.radius_disk > self.radius_axle + assert self.spring.height < self.housing_thickness + self.disk_thickness + assert self.housing_upper_carve_offset > 0 + assert self.spring_tail_hole_height > self.spring.thickness @property def neutral_movement_angle(self) -> Optional[float]: @@ -363,6 +368,12 @@ class DiskJoint(Model): @property def total_thickness(self) -> float: return self.housing_thickness * 2 + self.disk_thickness + @property + def disk_bot_thickness(self) -> float: + """ + Pads the bottom of the disk up to spring height + """ + return max(0, self.disk_thickness + self.spring.thickness - self.spring.height) @property def opening_span(self) -> float: @@ -373,7 +384,7 @@ class DiskJoint(Model): """ Distance between the spring track and the outside of the upper housing """ - return self.housing_thickness + self.disk_thickness - self.spring.height + return self.spring_tail_hole_height + (self.disk_thickness - self.disk_bot_thickness) - self.spring.height @property def housing_upper_dz(self) -> float: @@ -387,9 +398,9 @@ class DiskJoint(Model): Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, - height=self.disk_thickness, + height=self.spring.height-self.disk_bot_thickness, ) - .located(Cq.Location((0, self.spring.radius_inner, 0))) + .located(Cq.Location((0, self.spring.radius_inner, self.disk_bot_thickness))) .rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) ) @@ -427,6 +438,7 @@ class DiskJoint(Model): theta = math.radians(self.spring_slot_offset) plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0)) plane.workplane(offset=self.disk_thickness).tagPlane("mate_top") + plane.workplane(offset=self.disk_bot_thickness).tagPlane("mate_spring") result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot") return result @@ -474,11 +486,11 @@ class DiskJoint(Model): carve = ( Cq.Solid.makeCylinder( radius=self.spring.radius, - height=self.housing_thickness + height=self.spring_tail_hole_height, ).fuse(Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, - height=self.housing_thickness + height=self.spring_tail_hole_height, ).located(Cq.Location((0, -self.spring.radius, 0)))) ).rotate((0, 0, 0), (0, 0, 1), carve_angle) result = ( @@ -515,6 +527,7 @@ class DiskJoint(Model): result = ( result .union(wall, tol=TOL) + #.cut(carve) .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) ) return result.clean() @@ -541,7 +554,7 @@ class DiskJoint(Model): .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0) .constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0) .constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0) - .constrain(f"{disk}?mate_bot", f"{spring_name}?bot", "Plane", param=0) + .constrain(f"{disk}?mate_spring", f"{spring_name}?bot", "Plane") #.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0) #.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) #.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) @@ -608,13 +621,26 @@ class ElbowJoint(Model): material: Material = Material.RESIN_TRANSPERENT - angle_neutral: float = 0.0 + angle_neutral: float = 30.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 + def parent_arm_loc(self) -> Cq.Location: + """ + 2d Location of the centre of the arm surface on the parent side, assuming + axle is at position 0, and parent direction is -X + """ + return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) + def child_arm_loc(self) -> Cq.Location: + """ + 2d Location of the centre of the arm surface on the child side, assuming + axle is at position 0, and parent direction is -X + """ + return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180) + def lip(self) -> Cq.Workplane: holes = [ h @@ -649,8 +675,9 @@ class ElbowJoint(Model): Cq.Location((0, 0, 0), (0, 1, 0), 90) ) loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle) + loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( - loc_lip.inverse * Cq.Location((0, self.disk_joint.spring.radius_inner, 0)) * loc_disk) + loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() .add(self.lip().cut(disk_cut), name="lip", loc=loc_lip) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 587fd51..9df38e6 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -53,6 +53,7 @@ class WingProfile(Model): movement_angle=55, ), hole_diam=6.0, + angle_neutral=15.0, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 @@ -70,6 +71,7 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, + angle_neutral=30.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -86,6 +88,8 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float + elbow_rotate: float = -5 + wrist_rotate: float = 30.0 flip: bool = False @@ -94,6 +98,8 @@ 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) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias @@ -439,13 +445,12 @@ class WingProfile(Model): ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), ] h = self.elbow_height / 2 + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() tags_elbow = [ - ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( - -self.elbow_joint.parent_arm_radius, - h - self.elbow_h2)), - ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( - -self.elbow_joint.parent_arm_radius, - h + self.elbow_h2)), + ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, -self.elbow_h2)), + ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, self.elbow_h2)), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow @@ -507,24 +512,20 @@ class WingProfile(Model): return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() tags_elbow = [ - ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( - self.elbow_joint.child_arm_radius, - h - self.elbow_h2, - 180)), - ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( - self.elbow_joint.child_arm_radius, - h + self.elbow_h2, - 180)), + ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, self.elbow_h2)), + ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, -self.elbow_h2)), ] h = self.wrist_height / 2 + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc().flip_y() tags_wrist = [ - ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( - -self.wrist_joint.parent_arm_radius, - h - self.wrist_h2)), - ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( - -self.wrist_joint.parent_arm_radius, - h + self.wrist_h2)), + ("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)), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -564,7 +565,7 @@ class WingProfile(Model): o.generate(), point_tag=t, flipped=is_top == is_parent, - #rotate=True, + #rotate=not is_parent, ) return result.solve() @@ -579,15 +580,12 @@ class WingProfile(Model): 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_y() tags = [ - ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( - self.wrist_joint.child_arm_radius, - h - self.wrist_h2, - 180)), - ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( - self.wrist_joint.child_arm_radius, - h + self.wrist_h2, - 180)), + ("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)), ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) diff --git a/nhf/utils.py b/nhf/utils.py index be3912c..7cd0d42 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -82,6 +82,15 @@ def with_angle_2d(self: Cq.Location, angle: float) -> Tuple[float, float]: return Cq.Location.from2d(x, y, angle) Cq.Location.with_angle_2d = with_angle_2d +def flip_x(self: Cq.Location) -> Cq.Location: + (x, y), a = self.to2d() + return Cq.Location.from2d(-x, y, 90 - a) +Cq.Location.flip_x = flip_x +def flip_y(self: Cq.Location) -> Cq.Location: + (x, y), a = self.to2d() + return Cq.Location.from2d(x, -y, -a) +Cq.Location.flip_y = flip_y + ### Tags def tagPoint(self, tag: str):