cosplay: Touhou/Houjuu Nue #4
|
@ -3,6 +3,9 @@ import math
|
|||
import cadquery as Cq
|
||||
import nhf.parts.springs as springs
|
||||
from nhf import Role
|
||||
import nhf.utils
|
||||
|
||||
TOL = 1e-6
|
||||
|
||||
@dataclass
|
||||
class HirthJoint:
|
||||
|
@ -232,17 +235,31 @@ 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.
|
||||
|
||||
The rider part consists of:
|
||||
1. A cylinderical base
|
||||
2. A annular extrusion with the same radius as the base, but with slots
|
||||
carved in
|
||||
3. An annular rider
|
||||
|
||||
The track part consists of:
|
||||
1. A cylindrical base
|
||||
2. A slotted annular extrusion where the slot allows the spring to rest
|
||||
3. An outer and an inner annuli which forms a track the rider can move on
|
||||
"""
|
||||
|
||||
# Radius limit for rotating components
|
||||
radius: float = 40
|
||||
disk_height: float = 10
|
||||
track_disk_height: float = 10
|
||||
rider_disk_height: float = 8
|
||||
|
||||
radius_spring: float = 15
|
||||
radius_axle: float = 6
|
||||
|
||||
# Offset of the spring hole w.r.t. surface
|
||||
spring_hole_depth: float = 4
|
||||
# If true, cover the spring hole. May make it difficult to insert the spring
|
||||
# considering the stiffness of torsion spring steel.
|
||||
spring_hole_cover_track: bool = False
|
||||
spring_hole_cover_rider: bool = False
|
||||
|
||||
# Also used for the height of the hole for the spring
|
||||
spring_thickness: float = 2
|
||||
|
@ -253,13 +270,17 @@ class TorsionJoint:
|
|||
groove_radius_outer: float = 35
|
||||
groove_radius_inner: float = 20
|
||||
groove_depth: float = 5
|
||||
rider_gap: float = 2
|
||||
n_slots: float = 8
|
||||
rider_gap: float = 1
|
||||
rider_n_slots: float = 4
|
||||
|
||||
# Degrees of the first and last rider slots
|
||||
rider_slot_begin: float = 0
|
||||
rider_slot_span: float = 90
|
||||
|
||||
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
|
||||
|
@ -268,7 +289,7 @@ class TorsionJoint:
|
|||
|
||||
@property
|
||||
def total_height(self):
|
||||
return 2 * self.disk_height + self.spring_height
|
||||
return self.track_disk_height + self.rider_disk_height + self.spring_height
|
||||
|
||||
@property
|
||||
def _radius_spring_internal(self):
|
||||
|
@ -295,6 +316,10 @@ class TorsionJoint:
|
|||
r2 = -r2
|
||||
# This is (0, r2) and (l, r2) transformed by rotation matrix
|
||||
# [[c, s], [-s, c]]
|
||||
return [
|
||||
(0, 0, height),
|
||||
(c, s, height)
|
||||
]
|
||||
return [
|
||||
(s * r2, -s * l + c * r2, height),
|
||||
(c * l + s * r2, -s * l + c * r2, height),
|
||||
|
@ -320,14 +345,24 @@ class TorsionJoint:
|
|||
spring_hole_profile = (
|
||||
Cq.Sketch()
|
||||
.circle(self.radius)
|
||||
.polygon(self._slot_polygon(flip=False), mode='s')
|
||||
.circle(self.radius_spring, mode='s')
|
||||
)
|
||||
slot_height = self.spring_thickness
|
||||
if not self.spring_hole_cover_track:
|
||||
slot_height += self.groove_depth
|
||||
slot = (
|
||||
Cq.Workplane('XY')
|
||||
.sketch()
|
||||
.polygon(self._slot_polygon(flip=False))
|
||||
.finalize()
|
||||
.extrude(slot_height)
|
||||
.val()
|
||||
)
|
||||
result = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(
|
||||
radius=self.radius,
|
||||
height=self.disk_height,
|
||||
height=self.track_disk_height,
|
||||
centered=(True, True, False))
|
||||
.faces('>Z')
|
||||
.tag("spring")
|
||||
|
@ -340,9 +375,10 @@ class TorsionJoint:
|
|||
.extrude(self.groove_depth)
|
||||
.faces('>Z')
|
||||
.hole(self.radius_axle * 2)
|
||||
.cut(slot.moved(Cq.Location((0, 0, self.track_disk_height))))
|
||||
)
|
||||
# Insert directrix`
|
||||
result.polyline(self._directrix(self.disk_height),
|
||||
result.polyline(self._directrix(self.track_disk_height),
|
||||
forConstruction=True).tag("directrix")
|
||||
return result
|
||||
|
||||
|
@ -357,9 +393,9 @@ class TorsionJoint:
|
|||
.circle(self.radius_spring, mode='s')
|
||||
.parray(
|
||||
r=0,
|
||||
a1=0,
|
||||
da=360,
|
||||
n=self.n_slots)
|
||||
a1=self.rider_slot_begin,
|
||||
da=self.rider_slot_span,
|
||||
n=self.rider_n_slots)
|
||||
.each(slot, mode='s')
|
||||
#.circle(self._radius_wall, mode='a')
|
||||
)
|
||||
|
@ -367,43 +403,58 @@ class TorsionJoint:
|
|||
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
|
||||
if not self.spring_hole_cover_rider:
|
||||
contact_profile = (
|
||||
contact_profile
|
||||
.parray(
|
||||
r=0,
|
||||
a1=self.rider_slot_begin,
|
||||
da=self.rider_slot_span,
|
||||
n=self.rider_n_slots)
|
||||
.each(slot, mode='s')
|
||||
.reset()
|
||||
)
|
||||
#.circle(self._radius_wall, mode='a')
|
||||
middle_height = self.spring_height - self.groove_depth - self.rider_gap - self.spring_thickness
|
||||
result = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(
|
||||
radius=self.radius,
|
||||
height=self.disk_height,
|
||||
height=self.rider_disk_height,
|
||||
centered=(True, True, False))
|
||||
.faces('>Z')
|
||||
.tag("spring")
|
||||
.workplane()
|
||||
.placeSketch(wall_profile)
|
||||
.extrude(middle_height)
|
||||
.faces(tag="spring")
|
||||
.workplane()
|
||||
# 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")
|
||||
.workplane()
|
||||
.circle(self._radius_spring_internal)
|
||||
.extrude(self.spring_height)
|
||||
.faces('>Z')
|
||||
#.faces(tag="spring")
|
||||
#.workplane()
|
||||
.hole(self.radius_axle * 2)
|
||||
)
|
||||
for i in range(self.n_slots):
|
||||
theta = 2 * math.pi * i / self.n_slots
|
||||
result.polyline(self._directrix(self.disk_height, theta),
|
||||
theta_begin = math.radians(self.rider_slot_begin) + math.pi
|
||||
theta_span = math.radians(self.rider_slot_span)
|
||||
if abs(math.remainder(self.rider_slot_span, 360)) < TOL:
|
||||
theta_step = theta_span / self.rider_n_slots
|
||||
else:
|
||||
theta_step = theta_span / (self.rider_n_slots - 1)
|
||||
for i in range(self.rider_n_slots):
|
||||
theta = theta_begin - i * theta_step
|
||||
result.polyline(self._directrix(self.rider_disk_height, theta),
|
||||
forConstruction=True).tag(f"directrix{i}")
|
||||
return result
|
||||
|
||||
def rider_track_assembly(self):
|
||||
def rider_track_assembly(self, directrix=0):
|
||||
rider = self.rider()
|
||||
track = self.track()
|
||||
spring = self.spring()
|
||||
|
@ -412,10 +463,10 @@ class TorsionJoint:
|
|||
.add(spring, name="spring", color=Role.DAMPING.color)
|
||||
.add(track, name="track", color=Role.PARENT.color)
|
||||
.constrain("track?spring", "spring?top", "Plane")
|
||||
.constrain("track?directrix", "spring?directrix_bot", "Axis")
|
||||
.add(rider, name="rider", color=Role.CHILD.color)
|
||||
.constrain("rider?spring", "spring?bot", "Plane")
|
||||
.constrain("track?directrix", "spring?directrix_bot", "Axis")
|
||||
.constrain("rider?directrix0", "spring?directrix_top", "Axis")
|
||||
.constrain(f"rider?directrix{directrix}", "spring?directrix_top", "Axis")
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import unittest
|
||||
import cadquery as Cq
|
||||
from nhf.checks import binary_intersection
|
||||
from nhf.checks import binary_intersection, pairwise_intersection
|
||||
from nhf.parts import joints, handle, metric_threads
|
||||
|
||||
class TestJoints(unittest.TestCase):
|
||||
|
@ -22,12 +22,25 @@ class TestJoints(unittest.TestCase):
|
|||
"Hirth joint assembly must not have intersection")
|
||||
def test_joints_comma_assembly(self):
|
||||
joints.comma_assembly()
|
||||
|
||||
def test_torsion_joint(self):
|
||||
j = joints.TorsionJoint()
|
||||
assembly = j.rider_track_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.zlen, j.total_height)
|
||||
|
||||
def torsion_joint_collision_case(self, joint: joints.TorsionJoint, slot: int):
|
||||
assembly = joint.rider_track_assembly(slot)
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.zlen, joint.total_height)
|
||||
self.assertEqual(pairwise_intersection(assembly), [])
|
||||
|
||||
def test_torsion_joint_collision(self):
|
||||
j = joints.TorsionJoint()
|
||||
for slot in range(j.rider_n_slots):
|
||||
with self.subTest(slot=slot):
|
||||
self.torsion_joint_collision_case(j, slot)
|
||||
|
||||
|
||||
class TestHandle(unittest.TestCase):
|
||||
|
||||
|
|
Loading…
Reference in New Issue