feat: Elbow joint

This commit is contained in:
Leni Aniva 2024-07-11 22:29:05 -07:00
parent d43c1944a7
commit 9f9946569d
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
3 changed files with 179 additions and 11 deletions

View File

@ -23,6 +23,8 @@ from colorama import Fore, Style
import cadquery as Cq import cadquery as Cq
import nhf.checks as NC import nhf.checks as NC
TOL=1e-6
class TargetKind(Enum): class TargetKind(Enum):
STL = "stl", STL = "stl",
@ -70,7 +72,7 @@ class Target:
if isinstance(x, Cq.Workplane): if isinstance(x, Cq.Workplane):
x = x.val() x = x.val()
if isinstance(x, Cq.Assembly): if isinstance(x, Cq.Assembly):
x = x.toCompound() x = x.toCompound().fuse(tol=TOL)
x.exportStl(path, **self.kwargs) x.exportStl(path, **self.kwargs)
elif self.kind == TargetKind.DXF: elif self.kind == TargetKind.DXF:
assert isinstance(x, Cq.Workplane) assert isinstance(x, Cq.Workplane)

View File

@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional, Tuple from typing import Optional, Tuple
import cadquery as Cq import cadquery as Cq
from nhf import Role from nhf import Role
@ -8,6 +8,67 @@ import nhf.utils
TOL = 1e-6 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 @dataclass
class DiskJoint(Model): class DiskJoint(Model):
""" """
@ -76,8 +137,18 @@ class DiskJoint(Model):
@property @property
def housing_upper_carve_offset(self) -> float: 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 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 @property
def radius_spring_internal(self): def radius_spring_internal(self):
return self.radius_spring - self.spring_thickness return self.radius_spring - self.spring_thickness
@ -175,8 +246,8 @@ class DiskJoint(Model):
length=self.spring_tail_length, length=self.spring_tail_length,
width=self.spring_thickness, width=self.spring_thickness,
height=self.housing_thickness height=self.housing_thickness
).located(Cq.Location((0, self.radius_spring_internal, 0)))) ).located(Cq.Location((0, -self.radius_spring, 0))))
).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift) ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift)
result = ( result = (
Cq.Workplane('XY') Cq.Workplane('XY')
.cylinder( .cylinder(
@ -185,8 +256,8 @@ class DiskJoint(Model):
centered=(True, True, False), centered=(True, True, False),
) )
) )
result.faces(">Z").tag("mate") result.faces("<Z").tag("mate")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="-X")
result = result.faces(">Z").hole(self.radius_axle * 2) result = result.faces(">Z").hole(self.radius_axle * 2)
# tube which holds the spring interior # tube which holds the spring interior
@ -203,14 +274,12 @@ class DiskJoint(Model):
result = result.union(tube) result = result.union(tube)
wall = ( wall = (
self.wall() self.wall()
.rotate((0, 0, 0), (0, 0, 1), self.tongue_span) .located(Cq.Location((0, 0, -self.disk_thickness-self.wall_inset)))
.mirror("XY")
.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset)))
) )
result = ( result = (
result result
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
.union(wall, tol=TOL) .union(wall, tol=TOL)
.cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset))))
) )
return result return result
@ -256,6 +325,104 @@ class DiskJoint(Model):
) )
return result.solve() 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__': if __name__ == '__main__':
p = DiskJoint() p = DiskJoint()
p.build_all() p.build_all()

View File

@ -290,7 +290,6 @@ class WingProfile:
def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: 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_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 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 return elbow_x, elbow_y
def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]: 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 wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s