From 9f9946569d50b72cb55c4e27e9e2147a3ed93b74 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 11 Jul 2024 22:29:05 -0700 Subject: [PATCH] feat: Elbow joint --- nhf/build.py | 4 +- nhf/touhou/houjuu_nue/parts.py | 185 +++++++++++++++++++++++++++++++-- nhf/touhou/houjuu_nue/wing.py | 1 - 3 files changed, 179 insertions(+), 11 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index dcfbba7..9a16a1d 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -23,6 +23,8 @@ from colorama import Fore, Style import cadquery as Cq import nhf.checks as NC +TOL=1e-6 + class TargetKind(Enum): STL = "stl", @@ -70,7 +72,7 @@ class Target: if isinstance(x, Cq.Workplane): x = x.val() if isinstance(x, Cq.Assembly): - x = x.toCompound() + x = x.toCompound().fuse(tol=TOL) x.exportStl(path, **self.kwargs) elif self.kind == TargetKind.DXF: assert isinstance(x, Cq.Workplane) diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/parts.py index bdf458e..600350a 100644 --- a/nhf/touhou/houjuu_nue/parts.py +++ b/nhf/touhou/houjuu_nue/parts.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Tuple import cadquery as Cq from nhf import Role @@ -8,6 +8,67 @@ import nhf.utils TOL = 1e-6 +@dataclass +class Beam: + """ + A I-shaped spine with two feet + """ + + foot_length: float = 40.0 + foot_width: float = 20.0 + foot_height: float = 5.0 + spine_thickness: float = 4.0 + spine_length: float = 10.0 + total_height: float = 50.0 + + hole_diam: float = 8.0 + # distance between the centres of the two holes + hole_dist: float = 24.0 + + def __post_init__(self): + assert self.spine_height > 0 + assert self.hole_diam + self.hole_dist < self.foot_length + assert self.hole_dist - self.hole_diam >= self.spine_length + + @property + def spine_height(self): + return self.total_height - self.foot_height * 2 + + def foot(self) -> Cq.Workplane: + """ + just one foot + """ + dx = self.hole_dist / 2 + result = ( + Cq.Workplane('XZ') + .box(self.foot_length, self.foot_width, self.foot_height, + centered=(True, True, False)) + .faces(">Y") + .workplane() + .pushPoints([(dx, 0), (-dx, 0)]) + .hole(self.hole_diam) + ) + plane = result.faces(">Y").workplane() + plane.moveTo(dx, 0).tagPlane("conn1") + plane.moveTo(-dx, 0).tagPlane("conn0") + return result + + def beam(self) -> Cq.Assembly: + beam = ( + Cq.Workplane('XZ') + .box(self.spine_length, self.spine_thickness, self.spine_height) + ) + h = self.spine_height / 2 + self.foot_height + result = ( + Cq.Assembly() + .add(beam, name="beam") + .add(self.foot(), name="top", + loc=Cq.Location((0, h, 0))) + .add(self.foot(), name="bot", + loc=Cq.Location((0, -h, 0), (0, 0, 1), 180)) + ) + return result + @dataclass class DiskJoint(Model): """ @@ -76,8 +137,18 @@ class DiskJoint(Model): @property def housing_upper_carve_offset(self) -> float: + """ + Distance between the spring track and the outside of the upper housing + """ return self.housing_thickness + self.disk_thickness - self.spring_height + @property + def housing_upper_dz(self) -> float: + """ + Distance between the default upper housing location and the median line + """ + return self.total_thickness / 2 - self.housing_thickness + @property def radius_spring_internal(self): return self.radius_spring - self.spring_thickness @@ -175,8 +246,8 @@ class DiskJoint(Model): length=self.spring_tail_length, width=self.spring_thickness, height=self.housing_thickness - ).located(Cq.Location((0, self.radius_spring_internal, 0)))) - ).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift) + ).located(Cq.Location((0, -self.radius_spring, 0)))) + ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) result = ( Cq.Workplane('XY') .cylinder( @@ -185,8 +256,8 @@ class DiskJoint(Model): centered=(True, True, False), ) ) - result.faces(">Z").tag("mate") - result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result.faces("Z").hole(self.radius_axle * 2) # tube which holds the spring interior @@ -203,14 +274,12 @@ class DiskJoint(Model): result = result.union(tube) wall = ( self.wall() - .rotate((0, 0, 0), (0, 0, 1), self.tongue_span) - .mirror("XY") - .located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset))) + .located(Cq.Location((0, 0, -self.disk_thickness-self.wall_inset))) ) result = ( result + .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) .union(wall, tol=TOL) - .cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset)))) ) return result @@ -256,6 +325,104 @@ class DiskJoint(Model): ) return result.solve() +@dataclass +class ElbowJoint: + """ + Creates the elbow and wrist joints. + + This consists of a disk joint, where each side of the joint has mounting + holes for connection to the exoskeleton. Each side 2 mounting feet on the + top and bottom, and each foot has 2 holes. + + On the parent side, additional bolts are needed to clamp the two sides of + the housing together. + """ + + disk_joint: DiskJoint = field(default_factory=lambda: DiskJoint( + movement_angle=60, + )) + + # Distance between the child/parent arm to the centre + child_arm_radius: float = 40.0 + parent_arm_radius: float = 40.0 + + child_beam: Beam = field(default_factory=lambda: Beam()) + parent_beam: Beam = field(default_factory=lambda: Beam( + spine_thickness=8.0, + )) + parent_arm_span: float = 40.0 + # Angle of the beginning of the parent arm + parent_arm_angle: float = 180.0 + parent_binding_hole_radius: float = 30.0 + + # Size of the mounting holes + hole_diam: float = 8.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 + assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span + assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing + + def child_joint(self) -> Cq.Assembly: + angle = -self.disk_joint.tongue_span / 2 + dz = self.disk_joint.disk_thickness / 2 + result = ( + self.child_beam.beam() + .add(self.disk_joint.disk(), name="disk", + loc=Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + ) + return result + + def parent_joint_bot(self) -> Cq.Workplane: + return self.disk_joint.housing_lower() + + def parent_joint_top(self): + axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) + housing_dz = self.disk_joint.housing_upper_dz + conn_h = self.parent_beam.spine_thickness + connector = ( + Cq.Solid.makeCylinder( + height=conn_h, + radius=self.parent_arm_radius, + angleDegrees=self.parent_arm_span) + .cut(Cq.Solid.makeCylinder( + height=conn_h, + radius=self.disk_joint.radius_housing, + )) + .located(Cq.Location((0, 0, -conn_h / 2))) + .rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2) + ) + housing = self.disk_joint.housing_upper() + result = ( + Cq.Assembly() + .add(housing, name="housing", + loc=axial_offset * Cq.Location((0, 0, housing_dz))) + .add(connector, name="connector", + loc=axial_offset) + .add(self.parent_beam.beam(), name="beam") + ) + return result + + def assembly(self, angle: float = 0) -> Cq.Assembly: + da = self.disk_joint.tongue_span / 2 + result = ( + Cq.Assembly() + .add(self.child_joint(), name="child", color=Role.CHILD.color) + .add(self.parent_joint_bot(), name="parent_bot", color=Role.CASING.color) + .add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color) + .constrain("parent_bot", "Fixed") + ) + self.disk_joint.add_constraints( + result, + housing_lower="parent_bot", + housing_upper="parent_top/housing", + disk="child/disk", + angle=(0, 0, angle + da), + ) + return result.solve() + if __name__ == '__main__': p = DiskJoint() p.build_all() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b7dcd7c..feb57a8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -290,7 +290,6 @@ class WingProfile: 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