Cosplay/nhf/joints.py

381 lines
12 KiB
Python
Raw Normal View History

2024-06-26 12:57:22 -07:00
from dataclasses import dataclass
2024-06-19 15:54:09 -07:00
import math
2024-06-26 12:57:22 -07:00
import cadquery as Cq
import nhf.springs as NS
2024-06-19 15:54:09 -07:00
def hirth_joint(radius=60,
radius_inner=40,
base_height=20,
n_tooth=16,
tooth_height=16,
2024-06-19 16:14:49 -07:00
tooth_height_inner=2,
tol=0.01):
2024-06-19 15:54:09 -07:00
"""
Creates a cylindrical Hirth Joint
"""
# ensures secant doesn't blow up
assert n_tooth >= 5
2024-06-22 13:40:06 -07:00
assert radius > radius_inner
2024-06-19 15:54:09 -07:00
# angle of half of a single tooth
theta = math.pi / n_tooth
# Generate a tooth by lofting between two curves
inner_raise = (tooth_height - tooth_height_inner) / 2
# Outer tooth triangle spans a curve of length `2 pi r / n_tooth`. This
# creates the side profile (looking radially inwards) of each of the
# triangles.
outer = [
(radius * math.tan(theta), 0),
2024-06-19 16:14:49 -07:00
(radius * math.tan(theta) - tol, 0),
2024-06-19 15:54:09 -07:00
(0, tooth_height),
2024-06-19 16:14:49 -07:00
(-radius * math.tan(theta) + tol, 0),
2024-06-19 15:54:09 -07:00
(-radius * math.tan(theta), 0),
]
inner = [
(radius_inner * math.sin(theta), 0),
2024-06-19 16:14:49 -07:00
(radius_inner * math.sin(theta), inner_raise),
(0, inner_raise + tooth_height_inner),
(-radius_inner * math.sin(theta), inner_raise),
2024-06-19 15:54:09 -07:00
(-radius_inner * math.sin(theta), 0),
]
tooth = (
Cq.Workplane('YZ')
.polyline(inner)
.close()
.workplane(offset=radius - radius_inner)
.polyline(outer)
.close()
2024-06-19 16:14:49 -07:00
.loft(ruled=True, combine=True)
2024-06-19 15:54:09 -07:00
.val()
)
tooth_centre_radius = radius_inner * math.cos(theta)
teeth = (
Cq.Workplane('XY')
2024-06-19 16:14:49 -07:00
.polarArray(radius=tooth_centre_radius, startAngle=0, angle=360, count=n_tooth)
2024-06-19 15:54:09 -07:00
.eachpoint(lambda loc: tooth.located(loc))
.intersect(Cq.Solid.makeCylinder(
height=base_height + tooth_height,
radius=radius,
))
)
base = (
Cq.Workplane('XY')
.cylinder(
height=base_height,
radius=radius,
centered=(True, True, False))
.faces(">Z").tag("bore")
2024-06-22 13:40:06 -07:00
.union(teeth.val().move(Cq.Location((0,0,base_height))), tol=tol)
2024-06-19 15:54:09 -07:00
.clean()
)
#base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate")
2024-06-19 16:14:49 -07:00
base.polyline([(0, 0, base_height), (0, 0, base_height+tooth_height)], forConstruction=True).tag("mate")
2024-06-19 15:54:09 -07:00
return base
def hirth_assembly():
"""
Example assembly of two Hirth joints
"""
rotate = 180 / 16
obj1 = hirth_joint().faces(tag="bore").cboreHole(
diameter=10,
cboreDiameter=20,
cboreDepth=3)
obj2 = (
hirth_joint()
.rotate(
axisStartPoint=(0,0,0),
axisEndPoint=(0,0,1),
angleDegrees=rotate
)
)
result = (
Cq.Assembly()
.add(obj1, name="obj1", color=Cq.Color(0.8,0.8,0.5,0.3))
2024-06-19 16:14:49 -07:00
.add(obj2, name="obj2", color=Cq.Color(0.5,0.5,0.5,0.3))
.constrain("obj1?mate", "obj2?mate", "Plane")
2024-06-19 15:54:09 -07:00
.solve()
)
return result
2024-06-19 21:23:41 -07:00
def comma_joint(radius=30,
shaft_radius=10,
height=10,
flange=10,
flange_thickness=25,
n_serration=16,
serration_angle_offset=0,
serration_height=5,
serration_inner_radius=20,
serration_theta=2 * math.pi / 48,
serration_tilt=-30,
right_handed=False):
"""
Produces a "o_" shaped joint, with serrations to accomodate a torsion spring
"""
assert flange_thickness <= radius
flange_poly = [
(0, radius - flange_thickness),
(0, radius),
(flange + radius, radius),
(flange + radius, radius - flange_thickness)
]
if right_handed:
flange_poly = [(x, -y) for x,y in flange_poly]
sketch = (
Cq.Sketch()
.circle(radius)
.polygon(flange_poly, mode='a')
.circle(shaft_radius, mode='s')
)
serration_poly = [
(0, 0), (radius, 0),
(radius, radius * math.tan(serration_theta))
]
serration = (
Cq.Workplane('XY')
.sketch()
.polygon(serration_poly)
.circle(radius, mode='i')
.circle(serration_inner_radius, mode='s')
.finalize()
.extrude(serration_height)
.translate(Cq.Vector((-serration_inner_radius, 0, height)))
.rotate(
axisStartPoint=(0, 0, 0),
axisEndPoint=(0, 0, height),
angleDegrees=serration_tilt)
.val()
)
serrations = (
Cq.Workplane('XY')
.polarArray(radius=serration_inner_radius,
startAngle=0+serration_angle_offset,
angle=360+serration_angle_offset,
count=n_serration)
.eachpoint(lambda loc: serration.located(loc))
)
result = (
Cq.Workplane()
.add(sketch)
.extrude(height)
.union(serrations)
.clean()
)
result.polyline([
(0, 0, height - serration_height),
(0, 0, height + serration_height)],
forConstruction=True).tag("serrated")
result.polyline([
(0, radius, 0),
(flange + radius, radius, 0)],
forConstruction=True).tag("tail")
result.faces('>X').tag("tail_end")
return result
def comma_assembly():
joint1 = comma_joint()
joint2 = comma_joint()
2024-06-26 12:57:22 -07:00
spring = NS.torsion_spring()
2024-06-19 21:23:41 -07:00
result = (
Cq.Assembly()
.add(joint1, name="joint1", color=Cq.Color(0.8,0.8,0.5,0.3))
.add(joint2, name="joint2", color=Cq.Color(0.8,0.8,0.5,0.3))
.add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1))
2024-06-26 12:57:22 -07:00
.constrain("joint1?serrated", "spring?bot", "Plane")
.constrain("joint2?serrated", "spring?top", "Plane")
2024-06-19 21:23:41 -07:00
.constrain("joint1?tail", "FixedAxis", (1, 0, 0))
.constrain("joint2?tail", "FixedAxis", (-1, 0, 0))
.solve()
)
return result
2024-06-26 12:57:22 -07:00
@dataclass(frozen=True)
class TorsionJoint:
"""
This jonit consists of a rider puck on a track puck. IT is best suited if
the radius has to be small and vertical space is abundant.
"""
# Radius limit for rotating components
radius = 40
disk_height = 10
radius_spring = 15
radius_axle = 10
# Offset of the spring hole w.r.t. surface
spring_hole_depth = 4
# Also used for the height of the hole for the spring
spring_thickness = 2
spring_height = 15
spring_tail_length = 40
groove_radius_outer = 35
groove_radius_inner = 20
groove_depth = 5
rider_gap = 2
n_slots = 8
right_handed: bool = False
def __post_init__(self):
assert self.disk_height > self.spring_hole_depth
assert self.radius > self.groove_radius_outer
assert self.groove_radius_outer > self.groove_radius_inner
assert self.groove_radius_inner > self.radius_spring
assert self.spring_height > self.groove_depth, "Groove is too deep"
@property
def total_height(self):
return 2 * self.disk_height + self.spring_height
@property
def _radius_spring_internal(self):
return self.radius_spring - self.spring_thickness
def _slot_polygon(self, flip: bool=False):
r1 = self.radius_spring - self.spring_thickness
r2 = self.radius_spring
flip = flip != self.right_handed
if flip:
r1 = -r1
r2 = -r2
return [
(0, r2),
(self.spring_tail_length, r2),
(self.spring_tail_length, r1),
(0, r1),
]
def _directrix(self, height, theta=0):
c, s = math.cos(theta), math.sin(theta)
r2 = self.radius_spring
l = self.spring_tail_length
if self.right_handed:
r2 = -r2
# This is (0, r2) and (l, r2) transformed by rotation matrix
# [[c, s], [-s, c]]
return [
(s * r2, -s * l + c * r2, height),
(c * l + s * r2, -s * l + c * r2, height),
]
def spring(self):
return NS.torsion_spring(
radius=self.radius_spring,
height=self.spring_height,
thickness=self.spring_thickness,
tail_length=self.spring_tail_length,
)
def track(self):
groove_profile = (
Cq.Sketch()
.circle(self.radius)
.circle(self.groove_radius_outer, mode='s')
.circle(self.groove_radius_inner, mode='a')
.circle(self.radius_spring, mode='s')
)
spring_hole_profile = (
Cq.Sketch()
.circle(self.radius)
.polygon(self._slot_polygon(flip=False), mode='s')
.circle(self.radius_spring, mode='s')
)
result = (
Cq.Workplane('XY')
.cylinder(radius=self.radius, height=self.disk_height)
.faces('>Z')
.tag("spring")
.placeSketch(spring_hole_profile)
.extrude(self.spring_thickness)
# If the spring hole profile is not simply connected, this workplane
# will have to be created from the `spring-mate` face.
.faces('>Z')
.placeSketch(groove_profile)
.extrude(self.groove_depth)
.faces('>Z')
.hole(self.radius_axle)
)
# Insert directrix`
result.polyline(self._directrix(self.disk_height),
forConstruction=True).tag("directrix")
return result
def rider(self):
def slot(loc):
wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False))
face = Cq.Face.makeFromWires(wire)
return face.located(loc)
wall_profile = (
Cq.Sketch()
.circle(self.radius, mode='a')
.circle(self.radius_spring, mode='s')
.parray(
r=0,
a1=0,
da=360,
n=self.n_slots)
.each(slot, mode='s')
#.circle(self._radius_wall, mode='a')
)
contact_profile = (
Cq.Sketch()
.circle(self.groove_radius_outer, mode='a')
.circle(self.groove_radius_inner, mode='s')
#.circle(self._radius_wall, mode='a')
.parray(
r=0,
a1=0,
da=360,
n=self.n_slots)
.each(slot, mode='s')
)
middle_height = self.spring_height - self.groove_depth - self.rider_gap
result = (
Cq.Workplane('XY')
.cylinder(radius=self.radius, height=self.disk_height)
.faces('>Z')
.tag("spring")
.placeSketch(wall_profile)
.extrude(middle_height)
# The top face might not be in one piece.
#.faces('>Z')
.workplane(offset=middle_height)
.placeSketch(contact_profile)
.extrude(self.groove_depth + self.rider_gap)
.faces(tag="spring")
.circle(self._radius_spring_internal)
.extrude(self.spring_height)
.faces('>Z')
.hole(self.radius_axle)
)
for i in range(self.n_slots):
theta = 2 * math.pi * i / self.n_slots
result.polyline(self._directrix(self.disk_height, theta),
forConstruction=True).tag(f"directrix{i}")
return result
def rider_track_assembly(self):
rider = self.rider()
track = self.track()
spring = self.spring()
result = (
Cq.Assembly()
.add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1))
.add(track, name="track", color=Cq.Color(0.5,0.5,0.8,0.3))
.constrain("track?spring", "spring?top", "Plane")
.add(rider, name="rider", color=Cq.Color(0.8,0.8,0.5,0.3))
.constrain("rider?spring", "spring?bot", "Plane")
.constrain("track?directrix", "spring?directrix_bot", "Axis")
.constrain("rider?directrix0", "spring?directrix_top", "Axis")
.solve()
)
return result