diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 036a4f4..b69fb67 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -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 diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 1e70ba2..73b4cd7 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -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):