From 54593b9a4ef3eef93d525d21f1b55c7a57bb3718 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 12:15:47 -0700 Subject: [PATCH] feat: Shoulder parent joint --- nhf/parts/joints.py | 4 +- nhf/parts/test.py | 6 +++ nhf/touhou/houjuu_nue/__init__.py | 83 +++++++++++++++++++++++++------ nhf/touhou/houjuu_nue/test.py | 13 +++-- nhf/touhou/houjuu_nue/wing.py | 26 ++++++---- 5 files changed, 102 insertions(+), 30 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 2ebb543..a74b7fe 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -379,7 +379,9 @@ class TorsionJoint: forConstruction=True).tag("directrix") return result - def rider(self): + def rider(self, rider_slot_begin=None): + if not rider_slot_begin: + rider_slot_begin = self.rider_slot_begin def slot(loc): wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False)) face = Cq.Face.makeFromWires(wire) diff --git a/nhf/parts/test.py b/nhf/parts/test.py index bb05e20..93ab7c1 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -24,6 +24,7 @@ class TestJoints(unittest.TestCase): joints.comma_assembly() def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int): + assert 0 <= slot and slot < joint.rider_n_slots assembly = joint.rider_track_assembly(slot) bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.zlen, joint.total_height) @@ -45,6 +46,11 @@ class TestJoints(unittest.TestCase): spring_hole_cover_rider=True, ) self.torsion_joint_case(j, 1) + def test_torsion_joint_slot(self): + j = joints.TorsionJoint( + rider_slot_begin=90, + ) + self.torsion_joint_case(j, 1) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 9b5af33..32954d1 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -92,8 +92,10 @@ class Parameters(Model): wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - shoulder_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( - radius_axle=8 + shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + track_disk_height=5.0, + rider_disk_height=7.0, + radius_axle=8.0, )) # Two holes on each side (top and bottom) are used to attach the shoulder @@ -104,10 +106,10 @@ class Parameters(Model): """ Heights for various wing joints, where the numbers start from the first joint. """ + wing_s0_thickness: float = 40 + wing_s0_height: float = 100 wing_r1_height: float = 100 wing_r1_width: float = 400 - wing_r2_height: float = 100 - wing_r3_height: float = 100 trident_handle: Handle = field(default_factory=lambda: Handle( diam=38, @@ -267,18 +269,20 @@ class Parameters(Model): shoulder_attach_dist=self.shoulder_attach_dist, shoulder_attach_diam=self.shoulder_attach_diam, wall_thickness=self.wing_root_wall_thickness, + conn_height=self.wing_s0_height, + conn_thickness=self.wing_s0_thickness, ) - @target(name="shoulder") - def shoulder_parent_joint(self) -> Cq.Assembly: + @target(name="shoulder_parent") + def shoulder_parent_joint(self) -> Cq.Workplane: result = ( - self.shoulder_joint.rider() + self.shoulder_torsion_joint.rider() .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((100, 0, self.wing_root_wall_thickness)))) - .rect(30, 10, centered=(True, False)) + 'YZ', origin=Cq.Vector((90, 0, self.wing_root_wall_thickness)))) + .rect(25, 7, centered=(True, False)) .extrude("next") .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((60, 0, self.wing_root_wall_thickness)))) + 'YX', origin=Cq.Vector((55, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) .moveTo(0, self.shoulder_attach_dist) .hole(self.shoulder_attach_diam) @@ -287,6 +291,27 @@ 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 + core = ( + Cq.Workplane('XY') + .moveTo(0, 15) + .box(50, 40, 2 * dh, centered=(True, False, True)) + ) + 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", + loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) + .add(self.shoulder_torsion_joint.track(), name="track_bot", + loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + ) + return result + def wing_r1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in @@ -352,14 +377,44 @@ class Parameters(Model): result.solve() return result + 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 + def wing_r1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() .add(self.wing_root(), name="r1") - .add(self.shoulder_parent_joint(), name="shoulder_parent_top", - color=Material.RESIN_TRANSPARENT.color) - .constrain("r1/scaffold?conn_top0", "shoulder_parent_top?conn0", "Plane") - .constrain("r1/scaffold?conn_top1", "shoulder_parent_top?conn1", "Plane") + .add(self.shoulder_assembly(), name="shoulder") + .constrain("r1/scaffold", "Fixed") + .constrain("r1/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") + .constrain("r1/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") + .constrain("r1/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") + .constrain("r1/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") .solve() ) return result diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 0b63ecd..512b95b 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,6 +1,7 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M +from nhf.checks import pairwise_intersection class Test(unittest.TestCase): @@ -8,19 +9,23 @@ class Test(unittest.TestCase): p = M.Parameters() obj = p.hs_joint_parent() self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") + def test_shoulder_joint(self): + p = M.Parameters() + shoulder = p.shoulder_assembly() + assert isinstance(shoulder, Cq.Assembly) + self.assertEqual(pairwise_intersection(shoulder), []) + def test_wing_root(self): p = M.Parameters() obj = p.wing_root() + assert isinstance(obj, Cq.Assembly) #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") - bbox = obj.val().BoundingBox() + bbox = obj.toCompound().BoundingBox() msg = "Must fix 256^3 bbox" self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) - def test_wing_root(self): - p = M.Parameters() - p.wing_root() def test_wings_assembly(self): p = M.Parameters() p.wings_assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d9ab898..fe9c9e4 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -14,7 +14,7 @@ def wing_root_profiles( base_radius=40, middle_offset=30, middle_height=80, - conn_width=40, + conn_thickness=40, conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: assert base_sweep < 180 assert middle_offset > 0 @@ -74,7 +74,7 @@ def wing_root_profiles( # If the exterior sweep is theta', it has to satisfy # # sin(theta) * r2 + wall_thickness = sin(theta') * r1 - x, y = conn_width / 2, middle_height / 2 + x, y = conn_thickness / 2, middle_height / 2 t = wall_thickness dx = middle_offset middle = ( @@ -109,7 +109,7 @@ def wing_root_profiles( ) assert isinstance(middle, Cq.Wire) - x, y = conn_width / 2, conn_height / 2 + x, y = conn_thickness / 2, conn_height / 2 t = wall_thickness tip = ( Cq.Sketch() @@ -132,30 +132,32 @@ def wing_root(joint: HirthJoint, union_tol=1e-4, shoulder_attach_diam=8, shoulder_attach_dist=25, - conn_width=40, + conn_thickness=40, conn_height=100, wall_thickness=8) -> Cq.Assembly: """ Generate the contiguous components of the root wing segment """ tip_centre = Cq.Vector((-150, 0, -80)) + attach_theta = math.radians(5) + c, s = math.cos(attach_theta), math.sin(attach_theta) attach_points = [ - (15, 0), - (15 + shoulder_attach_dist, 0), + (15, 4), + (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s), ] root_profile, middle_profile, tip_profile = wing_root_profiles( - conn_width=conn_width, + conn_thickness=conn_thickness, conn_height=conn_height, wall_thickness=8, ) middle_profile = middle_profile.located(Cq.Location( - (-40, 0, -40), (0, 30, 0) + (-40, 0, -40), (0, 1, 0), 30 )) antetip_profile = tip_profile.located(Cq.Location( - (-95, 0, -75), (0, 60, 0) + (-95, 0, -75), (0, 1, 0), 60 )) tip_profile = tip_profile.located(Cq.Location( - tip_centre, (0, 90, 0) + tip_centre, (0, 1, 0), 90 )) profiles = [ root_profile, @@ -201,9 +203,11 @@ def wing_root(joint: HirthJoint, Cq.Vector((0, y, 0))) ) ) + if side == "bottom": + side = "bot" for i, (px, py) in enumerate(attach_points): tag = f"conn_{side}{i}" - plane.moveTo(px, py).tagPlane(tag) + plane.moveTo(px, -py if side == "top" else py).tagPlane(tag) result.faces("X").tag("conn")