cosplay: Touhou/Houjuu Nue #4
229
nhf/joints.py
229
nhf/joints.py
|
@ -1,5 +1,7 @@
|
|||
import cadquery as Cq
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import cadquery as Cq
|
||||
import nhf.springs as NS
|
||||
|
||||
def hirth_joint(radius=60,
|
||||
radius_inner=40,
|
||||
|
@ -174,54 +176,205 @@ def comma_joint(radius=30,
|
|||
result.faces('>X').tag("tail_end")
|
||||
return result
|
||||
|
||||
def torsion_spring(radius=12,
|
||||
height=20,
|
||||
thickness=2,
|
||||
omega=90,
|
||||
tail_length=25):
|
||||
"""
|
||||
Produces a torsion spring with abridged geometry since sweep is very slow in
|
||||
cq-editor.
|
||||
"""
|
||||
base = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(height=height, radius=radius,
|
||||
centered=(True, True, False))
|
||||
)
|
||||
base.faces(">Z").tag("mate_top")
|
||||
base.faces("<Z").tag("mate_bottom")
|
||||
result = (
|
||||
base
|
||||
.cylinder(height=height, radius=radius - thickness, combine='s',
|
||||
centered=(True, True, True))
|
||||
.transformed(
|
||||
offset=(0, radius-thickness),
|
||||
rotate=(0, 0, 0))
|
||||
.box(length=tail_length, width=thickness, height=thickness, centered=False)
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.transformed(
|
||||
offset=(0, 0, height - thickness),
|
||||
rotate=(0, 0, omega))
|
||||
.center(-tail_length, radius-thickness)
|
||||
.box(length=tail_length, width=thickness, height=thickness, centered=False)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def comma_assembly():
|
||||
joint1 = comma_joint()
|
||||
joint2 = comma_joint()
|
||||
spring = torsion_spring()
|
||||
spring = NS.torsion_spring()
|
||||
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))
|
||||
.constrain("joint1?serrated", "spring?mate_bottom", "Plane")
|
||||
.constrain("joint2?serrated", "spring?mate_top", "Plane")
|
||||
.constrain("joint1?serrated", "spring?bot", "Plane")
|
||||
.constrain("joint2?serrated", "spring?top", "Plane")
|
||||
.constrain("joint1?tail", "FixedAxis", (1, 0, 0))
|
||||
.constrain("joint2?tail", "FixedAxis", (-1, 0, 0))
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
||||
@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
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import math
|
||||
import cadquery as Cq
|
||||
|
||||
def torsion_spring(radius=12,
|
||||
height=20,
|
||||
thickness=2,
|
||||
omega=90,
|
||||
tail_length=25):
|
||||
"""
|
||||
Produces a torsion spring with abridged geometry since sweep is very slow in
|
||||
cq-editor.
|
||||
"""
|
||||
base = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(height=height, radius=radius,
|
||||
centered=(True, True, False))
|
||||
)
|
||||
base.faces(">Z").tag("top")
|
||||
base.faces("<Z").tag("bot")
|
||||
result = (
|
||||
base
|
||||
.cylinder(height=height, radius=radius - thickness, combine='s',
|
||||
centered=(True, True, True))
|
||||
.transformed(
|
||||
offset=(0, radius-thickness),
|
||||
rotate=(0, 0, 0))
|
||||
.box(length=tail_length, width=thickness, height=thickness, centered=False)
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.transformed(
|
||||
offset=(0, 0, height - thickness),
|
||||
rotate=(0, 0, omega))
|
||||
.center(-tail_length, radius-thickness)
|
||||
.box(length=tail_length, width=thickness, height=thickness, centered=False)
|
||||
)
|
||||
result.polyline([(0, radius, 0), (tail_length, radius, 0)],
|
||||
forConstruction=True).tag("directrix_bot")
|
||||
c, s = math.cos(omega * math.pi / 180), math.sin(omega * math.pi / 180)
|
||||
result.polyline([
|
||||
(s * tail_length, c * radius - s * tail_length, height),
|
||||
(c * tail_length + s * radius, c * radius - s * tail_length, height)],
|
||||
forConstruction=True).tag("directrix_top")
|
||||
return result
|
|
@ -15,6 +15,11 @@ class TestJoints(unittest.TestCase):
|
|||
nhf.joints.hirth_assembly()
|
||||
def test_joints_comma_assembly(self):
|
||||
nhf.joints.comma_assembly()
|
||||
def test_torsion_joint(self):
|
||||
j = nhf.joints.TorsionJoint()
|
||||
assembly = j.rider_track_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.zlen, j.total_height)
|
||||
|
||||
|
||||
class TestHandle(unittest.TestCase):
|
||||
|
|
Loading…
Reference in New Issue