diff --git a/nhf/joints.py b/nhf/joints.py index 554d7b6..e7df7e9 100644 --- a/nhf/joints.py +++ b/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(" 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 diff --git a/nhf/springs.py b/nhf/springs.py new file mode 100644 index 0000000..06e14b5 --- /dev/null +++ b/nhf/springs.py @@ -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("