diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 5990e8a..c37e0a9 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -100,7 +100,7 @@ class HirthJoint: ( result .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) - .tag("directrix") + .tag("dir") ) return result @@ -132,7 +132,7 @@ class HirthJoint: .add(obj2, name="obj2", color=Role.CHILD.color) .constrain("obj1", "Fixed") .constrain("obj1?mate", "obj2?mate", "Plane") - .constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle) + .constrain("obj1?dir", "obj2?dir", "Axis", param=angle) .solve() ) return result @@ -249,7 +249,8 @@ class TorsionJoint: """ # Radius limit for rotating components - radius: float = 40 + radius_track: float = 40 + radius_rider: float = 38 track_disk_height: float = 10 rider_disk_height: float = 8 @@ -281,7 +282,8 @@ class TorsionJoint: def __post_init__(self): - assert self.radius > self.groove_radius_outer + assert self.radius_track > self.groove_radius_outer + assert self.radius_rider > 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" @@ -289,8 +291,18 @@ class TorsionJoint: @property def total_height(self): + """ + Total height counting from bottom to top + """ return self.track_disk_height + self.rider_disk_height + self.spring_height + @property + def radius(self): + """ + Maximum radius of this joint + """ + return max(self.radius_rider, self.radius_track) + @property def _radius_spring_internal(self): return self.radius_spring - self.spring_thickness @@ -335,14 +347,14 @@ class TorsionJoint: # TODO: Cover outer part of track only. Can we do this? groove_profile = ( Cq.Sketch() - .circle(self.radius) + .circle(self.radius_track) .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) + .circle(self.radius_track) .circle(self.radius_spring, mode='s') ) slot_height = self.spring_thickness @@ -359,7 +371,7 @@ class TorsionJoint: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.radius_track, height=self.track_disk_height, centered=(True, True, False)) .faces('>Z') @@ -375,12 +387,12 @@ class TorsionJoint: .hole(self.radius_axle * 2) .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) ) - # Insert directrix` + # Insert directrix result.polyline(self._directrix(self.track_disk_height), - forConstruction=True).tag("directrix") + forConstruction=True).tag("dir") return result - def rider(self, rider_slot_begin=None): + def rider(self, rider_slot_begin=None, reverse_directrix_label=False): if not rider_slot_begin: rider_slot_begin = self.rider_slot_begin def slot(loc): @@ -389,11 +401,11 @@ class TorsionJoint: return face.located(loc) wall_profile = ( Cq.Sketch() - .circle(self.radius, mode='a') + .circle(self.radius_rider, mode='a') .circle(self.radius_spring, mode='s') .parray( r=0, - a1=self.rider_slot_begin, + a1=rider_slot_begin, da=self.rider_slot_span, n=self.rider_n_slots) .each(slot, mode='s') @@ -409,7 +421,7 @@ class TorsionJoint: contact_profile .parray( r=0, - a1=self.rider_slot_begin, + a1=rider_slot_begin, da=self.rider_slot_span, n=self.rider_n_slots) .each(slot, mode='s') @@ -420,7 +432,7 @@ class TorsionJoint: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.radius_rider, height=self.rider_disk_height, centered=(True, True, False)) .faces('>Z') @@ -442,16 +454,17 @@ class TorsionJoint: #.workplane() .hole(self.radius_axle * 2) ) - theta_begin = math.radians(self.rider_slot_begin) + math.pi + theta_begin = math.radians(rider_slot_begin) 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 + theta = theta_begin + i * theta_step + j = self.rider_n_slots - i - 1 if reverse_directrix_label else i result.polyline(self._directrix(self.rider_disk_height, theta), - forConstruction=True).tag(f"directrix{i}") + forConstruction=True).tag(f"dir{j}") return result def rider_track_assembly(self, directrix=0): @@ -462,11 +475,26 @@ class TorsionJoint: Cq.Assembly() .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(f"rider?directrix{directrix}", "spring?directrix_top", "Axis") - .solve() + .add(rider, name="rider", color=Role.PARENT.color) + ) + TorsionJoint.add_constraints(result, + rider="rider", track="track", spring="spring", + directrix=directrix) + return result.solve() + + @staticmethod + def add_constraints(assembly: Cq.Assembly, + rider: str, track: str, spring: str, + directrix: int = 0): + """ + Add the necessary constraints to a RT assembly + """ + ( + assembly + .constrain(f"{track}?spring", f"{spring}?top", "Plane") + .constrain(f"{track}?dir", f"{spring}?dir_top", + "Axis", param=0) + .constrain(f"{rider}?spring", f"{spring}?bot", "Plane") + .constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot", + "Axis", param=0) ) - return result diff --git a/nhf/parts/springs.py b/nhf/parts/springs.py index 7ce0530..c695d23 100644 --- a/nhf/parts/springs.py +++ b/nhf/parts/springs.py @@ -46,11 +46,14 @@ def torsion_spring(radius=12, centered=False) ) r = -radius if right_handed else radius - result.polyline([(0, r, 0), (tail_length, r, 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 * r - s * tail_length, height), - (c * tail_length + s * r, c * r - s * tail_length, height)], - forConstruction=True).tag("directrix_top") + plane = result.copyWorkplane(Cq.Workplane('XY')) + plane.polyline([(0, r, 0), (tail_length, r, 0)], + forConstruction=True).tag("dir_bot") + omega = math.radians(omega) + c, s = math.cos(omega), math.sin(omega) + l = -tail_length + plane.polyline([ + (-s * r, c * r, height), + (c * l - s * r, c * r + s * l, height)], + forConstruction=True).tag("dir_top") return result diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 4445d57..3b40ce4 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -35,11 +35,13 @@ class TestJoints(unittest.TestCase): def test_torsion_joint(self): j = joints.TorsionJoint() for slot in range(j.rider_n_slots): - with self.subTest(slot=slot): + with self.subTest(slot=slot, right_handed=False): self.torsion_joint_case(j, slot) def test_torsion_joint_right_handed(self): j = joints.TorsionJoint(right_handed=True) - self.torsion_joint_case(j, 1) + for slot in range(j.rider_n_slots): + with self.subTest(slot=slot, right_handed=True): + self.torsion_joint_case(j, slot) def test_torsion_joint_covered(self): j = joints.TorsionJoint( spring_hole_cover_track=True, diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 564a7fc..1c210b8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -30,7 +30,6 @@ s1, s2, s3. The joints are named (from root to tip) shoulder, elbow, wrist in analogy with human anatomy. """ from dataclasses import dataclass, field -import unittest import cadquery as Cq from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly @@ -93,6 +92,9 @@ class Parameters(Model): wing_root_wall_thickness: float = 8 shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + radius_track=35, + radius_rider=35, + groove_radius_outer=32, track_disk_height=5.0, rider_disk_height=7.0, radius_axle=8.0, @@ -114,6 +116,9 @@ class Parameters(Model): wing_s1_thickness: float = 20 wing_s1_spacer_thickness: float = 25.4 / 8 wing_s1_spacer_width: float = 20 + wing_s1_spacer_hole_diam: float = 8 + wing_s1_shoulder_spacer_hole_dist: float = 20 + wing_s1_shoulder_spacer_width: float = 60 trident_handle: Handle = field(default_factory=lambda: Handle( diam=38, @@ -132,6 +137,8 @@ class Parameters(Model): super().__init__(name="houjuu-nue") assert self.wing_root_radius > self.hs_hirth_joint.radius,\ "Wing root must be large enough to accomodate joint" + assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \ + "Spacer holes are too close to each other" @target(name="trident/handle-connector") def handle_connector(self): @@ -304,14 +311,32 @@ class Parameters(Model): conn_thickness=self.wing_s0_thickness, ) - @target(name="shoulder_parent") - def shoulder_parent_joint(self) -> Cq.Workplane: + @target(name="shoulder_joint_parent") + def shoulder_joint_parent(self) -> Cq.Workplane: + joint = self.shoulder_torsion_joint + # Thickness of the lip connecting this joint to the wing root + lip_thickness = 10 + lip_width = 25 + lip_guard_ext = 40 + lip_guard_height = self.wing_root_wall_thickness + lip_thickness + assert lip_guard_ext > joint.radius_track + + lip_guard = ( + Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) + .located(Cq.Location((0, -lip_width/2 , 0))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) + ) result = ( - self.shoulder_torsion_joint.rider() + joint.track() + .union(lip_guard, tol=1e-6) + + # Extrude the handle .copyWorkplane(Cq.Workplane( 'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) - .rect(25, 7, centered=(True, False)) + .rect(lip_width, lip_thickness, centered=(True, False)) .extrude("next") + + # Connector holes on the lip .copyWorkplane(Cq.Workplane( 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) @@ -322,27 +347,117 @@ class Parameters(Model): result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') return result - @target(name="shoulder_child") - def shoulder_child_joint(self) -> Cq.Assembly: - # FIXME: half of conn_height - h = 100 / 2 - dh = h - self.shoulder_torsion_joint.total_height + @target(name="shoulder_joint_child") + def shoulder_joint_child(self) -> Cq.Assembly: + """ + Creates the top/bottom shoulder child joint + """ + + joint = self.shoulder_torsion_joint + # Half of the height of the bridging cylinder + dh = self.wing_s0_height / 2 - joint.total_height + core_start_angle = 30 + core_end_angle1 = 90 + core_end_angle2 = 180 + core_thickness = 2 + + core_profile1 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) + core_profile2 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) core = ( Cq.Workplane('XY') - .moveTo(0, 15) - .box(50, 40, 2 * dh, centered=(True, False, True)) + .placeSketch(core_profile1) + .toPending() + .extrude(dh * 2) + .copyWorkplane(Cq.Workplane('XY')) + .placeSketch(core_profile2) + .toPending() + .extrude(dh * 2) + .translate(Cq.Vector(0, 0, -dh)) ) + # Create the upper and lower lips + lip_height = self.wing_s1_thickness + lip_thickness = joint.rider_disk_height + lip_ext = 40 + joint.radius_rider + hole_dx = self.wing_s1_shoulder_spacer_hole_dist + assert lip_height / 2 <= joint.radius_rider + lip = ( + Cq.Workplane('XY') + .box(lip_ext, lip_height, lip_thickness, + centered=(False, True, False)) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder(radius=joint.radius_rider, height=lip_thickness, + centered=(True, True, False), + combine='cut') + .faces(">Z") + .workplane() + ) + hole_x = lip_ext - hole_dx / 2 + for i in range(2): + plane = ( + lip + .moveTo(hole_x - i * hole_dx, 0) + ) + lip = plane.hole(self.wing_s1_spacer_hole_diam) + plane.tagPlane(f"hole{i}") + loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) - .add(self.shoulder_torsion_joint.track(), name="track_top", + .add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top", loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) - .add(self.shoulder_torsion_joint.track(), name="track_bot", + .add(joint.rider(rider_slot_begin=180), name="rider_bot", loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + .add(lip, name="lip_top", + loc=Cq.Location((0, 0, dh))) + .add(lip, name="lip_bot", + loc=Cq.Location((0, 0, -dh)) * loc_rotate) ) return result + @assembly() + def shoulder_assembly(self) -> Cq.Assembly: + directrix = 0 + result = ( + Cq.Assembly() + .add(self.shoulder_joint_child(), name="child", + color=Role.CHILD.color) + .constrain("child/core", "Fixed") + .add(self.shoulder_torsion_joint.spring(), name="spring_top", + color=Role.DAMPING.color) + .add(self.shoulder_joint_parent(), name="parent_top", + color=Role.PARENT.color) + .add(self.shoulder_torsion_joint.spring(), name="spring_bot", + color=Role.DAMPING.color) + .add(self.shoulder_joint_parent(), name="parent_bot", + color=Role.PARENT.color) + ) + TorsionJoint.add_constraints(result, + rider="child/rider_top", + track="parent_top", + spring="spring_top", + directrix=directrix) + TorsionJoint.add_constraints(result, + rider="child/rider_bot", + track="parent_bot", + spring="spring_bot", + directrix=directrix) + return result.solve() + @target(name="wing/s1-spacer", kind=TargetKind.DXF) def wing_s1_spacer(self) -> Cq.Workplane: result = ( @@ -357,6 +472,36 @@ class Parameters(Model): result.faces(">Y").tag("dir") return result + @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) + def wing_s1_shoulder_spacer(self) -> Cq.Workplane: + dx = self.wing_s1_shoulder_spacer_hole_dist + result = ( + Cq.Workplane('XZ') + .sketch() + .rect(self.wing_s1_shoulder_spacer_width, + self.wing_s1_thickness) + .push([ + (0, 0), + (dx, 0), + ]) + .circle(self.wing_s1_spacer_hole_diam / 2, mode='s') + .finalize() + .extrude(self.wing_s1_spacer_thickness) + ) + # Tag the mating surfaces to be glued + result.faces("Z").tag("mate2") + + # Tag the directrix + result.faces(">Y").tag("dir") + + # Tag the holes + plane = result.faces(">Y").workplane() + # Side closer to the parent is 0 + plane.moveTo(dx, 0).tagPlane("hole0") + plane.tagPlane("hole1") + return result + @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: return MW.wing_r1s1_profile() @@ -379,35 +524,6 @@ class Parameters(Model): plane.moveTo(px, sign * py).tagPlane(name) return result - @assembly() - def shoulder_assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.shoulder_child_joint(), name="child", - color=Role.CHILD.color) - .constrain("child/core", "Fixed") - # Top parent joint - .add(self.shoulder_torsion_joint.spring(), name="spring_top", - color=Role.DAMPING.color) - .constrain("child/track_top?spring", "spring_top?top", "Plane") - .constrain("child/track_top?directrix", "spring_top?directrix_bot", "Axis") - .add(self.shoulder_parent_joint(), name="parent_top", - color=Role.PARENT.color) - .constrain("parent_top?spring", "spring_top?bot", "Plane") - .constrain("parent_top?directrix0", "spring_top?directrix_top", "Axis") - # Bottom parent joint - .add(self.shoulder_torsion_joint.spring(), name="spring_bot", - color=Role.DAMPING.color) - .constrain("child/track_bot?spring", "spring_bot?top", "Plane") - .constrain("child/track_bot?directrix", "spring_bot?directrix_bot", "Axis") - .add(self.shoulder_parent_joint(), name="parent_bot", - color=Role.PARENT.color) - .constrain("parent_bot?spring", "spring_bot?bot", "Plane") - .constrain("parent_bot?directrix0", "spring_bot?directrix_top", "Axis") - .solve() - ) - return result - @assembly() def wing_r1s1_assembly(self) -> Cq.Assembly: result = ( @@ -427,6 +543,13 @@ class Parameters(Model): .constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane") .constrain(f"panel_back?{tag}", f"{tag}_spacer?mate2", "Plane") ) + ( + result + .add(self.shoulder_assembly(), name="shoulder") + .constrain("shoulder_bot_spacer?dir", + "shoulder/child/core?mate_bot", + "Plane") + ) result.solve() return result