From 641755314eb8cef17caf56c96ddb767279a15ebf Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 12 Jul 2024 23:16:04 -0700 Subject: [PATCH] refactor: Factor out parts of the wing assembly --- nhf/materials.py | 1 + nhf/touhou/houjuu_nue/__init__.py | 430 ++++++++---------- nhf/touhou/houjuu_nue/{parts.py => joints.py} | 220 ++++++++- nhf/touhou/houjuu_nue/test.py | 20 +- nhf/touhou/houjuu_nue/wing.py | 10 +- nhf/utils.py | 55 ++- 6 files changed, 469 insertions(+), 267 deletions(-) rename nhf/touhou/houjuu_nue/{parts.py => joints.py} (64%) diff --git a/nhf/materials.py b/nhf/materials.py index 6108dc1..a95a7eb 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -21,6 +21,7 @@ class Role(Enum): STRUCTURE = _color('gray', 0.4) DECORATION = _color('lightseagreen', 0.4) ELECTRONIC = _color('mediumorchid', 0.5) + MARKER = _color('white', 1.0) def __init__(self, color: Cq.Color): self.color = color diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 1eeb1e8..7cdba60 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -37,6 +37,7 @@ from nhf.parts.joints import HirthJoint, TorsionJoint from nhf.parts.handle import Handle, BayonetMount import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT +import nhf.touhou.houjuu_nue.joints as MJ import nhf.utils @dataclass @@ -88,33 +89,21 @@ class Parameters(Model): hs_joint_axis_cbore_depth: float = 3 wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( - shoulder_height = 80, - elbow_height = 100, + shoulder_height=100.0, + elbow_height=110.0, )) # Exterior radius of the wing root assembly wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( - radius_track=18, - radius_rider=18, - groove_radius_outer=16, - groove_radius_inner=13, - track_disk_height=5.0, - rider_disk_height=5.0, - # M8 Axle - radius_axle=3.0, - # inner diameter = 9 - radius_spring=9/2 + 1.2, - spring_thickness=1.3, - spring_height=7.5, + shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint( + shoulder_height=100.0, + )) + elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( + )) + wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( )) - - # Two holes on each side (top and bottom) are used to attach the shoulder - # joint. This governs the distance between these two holes - shoulder_attach_dist: float = 25 - shoulder_attach_diam: float = 8 """ Heights for various wing joints, where the numbers start from the first @@ -325,8 +314,8 @@ class Parameters(Model): """ return MW.wing_root( joint=self.hs_hirth_joint, - shoulder_attach_dist=self.shoulder_attach_dist, - shoulder_attach_diam=self.shoulder_attach_diam, + shoulder_attach_dist=self.shoulder_joint.attach_dist, + shoulder_attach_diam=self.shoulder_joint.attach_diam, wall_thickness=self.wing_root_wall_thickness, conn_height=self.wing_profile.shoulder_height, conn_thickness=self.wing_s0_thickness, @@ -334,169 +323,25 @@ class Parameters(Model): @target(name="wing/proto-shoulder-joint-parent", prototype=True) def proto_shoulder_joint_parent(self): - return self.shoulder_torsion_joint.track() + return self.shoulder_joint.torsion_joint.track() @target(name="wing/proto-shoulder-joint-child", prototype=True) def proto_shoulder_joint_child(self): - return self.shoulder_torsion_joint.rider() - - @target(name="wing/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 = ( - 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(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) - .moveTo(0, self.shoulder_attach_dist) - .hole(self.shoulder_attach_diam) - ) - result.moveTo(0, 0).tagPlane('conn0') - result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') - return result - - @property - def shoulder_joint_child_height(self) -> float: - """ - Calculates the y distance between two joint surfaces on the child side - of the shoulder joint. - """ - joint = self.shoulder_torsion_joint - return self.wing_profile.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height - - @target(name="wing/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_profile.shoulder_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') - .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): - x = hole_x - i * hole_dx - lip = lip.moveTo(x, 0).hole(self.wing_s1_spacer_hole_diam) - for i in range(2): - x = hole_x - i * hole_dx - ( - lip - .moveTo(x, 0) - .tagPlane(f"conn{1 - i}") - ) - - loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) - result = ( - Cq.Assembly() - .add(core, name="core", loc=Cq.Location()) - .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(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 - + return self.shoulder_joint.torsion_joint.rider() @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) + def shoulder_assembly(self): + return self.shoulder_joint.assembly( + wing_root_wall_thickness=self.wing_root_wall_thickness, + lip_height=self.wing_s1_thickness, + hole_dist=self.wing_s1_shoulder_spacer_hole_dist, + spacer_hole_diam=self.wing_s1_spacer_hole_diam, ) - 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() + @assembly() + def elbow_assembly(self): + return self.elbow_joint.assembly() + @assembly() + def wrist_assembly(self): + return self.wrist_joint.assembly() + @target(name="wing/s1-spacer", kind=TargetKind.DXF) def wing_s1_spacer(self) -> Cq.Workplane: @@ -515,61 +360,93 @@ class Parameters(Model): @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) def wing_s1_shoulder_spacer(self) -> Cq.Workplane: """ - The mate tags are on the side closer to the holes. + Creates a rectangular spacer. This could be cut from acrylic. + + There are two holes on the top of the spacer. With the holes """ dx = self.wing_s1_shoulder_spacer_hole_dist h = self.wing_s1_spacer_thickness + length = self.wing_s1_shoulder_spacer_width + hole_diam = self.wing_s1_spacer_hole_diam + assert dx + hole_diam < length / 2 result = ( - Cq.Workplane('XZ') + Cq.Workplane('XY') .sketch() - .rect(self.wing_s1_shoulder_spacer_width, - self.wing_s1_thickness) + .rect(length, self.wing_s1_thickness) .push([ (0, 0), (dx, 0), ]) - .circle(self.wing_s1_spacer_hole_diam / 2, mode='s') + .circle(hole_diam / 2, mode='s') .finalize() .extrude(h) ) # Tag the mating surfaces to be glued - result.faces("Z").workplane().moveTo(0, -h).tagPlane("weld2") + result.faces("Y").workplane().moveTo(-length / 2, h).tagPlane("right") # Tag the directrix - result.faces("Z").tag("dir") # Tag the holes - plane = result.faces(">Y").workplane() + plane = result.faces(">Z").workplane() # Side closer to the parent is 0 - plane.moveTo(-dx, 0).tagPlane("conn0") + plane.moveTo(dx, 0).tagPlane("conn0") plane.tagPlane("conn1") return result + def assembly_insert_shoulder_spacer( + self, + assembly, + spacer, + point_tag: str, + front_tag: str = "panel_front", + back_tag: str = "panel_back", + flipped: bool = False, + ): + """ + For a child joint facing up, front panel should be on the right, back + panel on the left + """ + site_front, site_back = "right", "left" + if flipped: + site_front, site_back = site_back, site_front + angle = 0 + ( + assembly + .add(spacer, + name=f"{point_tag}_spacer", + color=self.material_bracket.color) + .constrain(f"{front_tag}?{point_tag}", + f"{point_tag}_spacer?{site_front}", "Plane") + .constrain(f"{back_tag}?{point_tag}", + f"{point_tag}_spacer?{site_back}", "Plane") + .constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir", + "Axis", param=angle) + ) @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: + """ + FIXME: Output individual segment profiles + """ return self.wing_profile.profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: - profile = self.wing_r1s1_profile() - w = self.wing_s1_shoulder_spacer_width / 2 - h = (self.wing_profile.shoulder_height - self.shoulder_joint_child_height) / 2 - anchors = [ - ("shoulder_top", w, h + self.shoulder_joint_child_height), - ("shoulder_bot", w, h), - ("middle", 50, -20), - ("tip", 270, 50), - ] - result = ( - Cq.Workplane("XY") - .placeSketch(profile) - .extrude(self.panel_thickness) + return self.wing_profile.surface_s1( + thickness=self.panel_thickness, + shoulder_joint_child_height=self.shoulder_joint.child_height, + front=front, + ) + def wing_r1s2_panel(self, front=True) -> Cq.Workplane: + return self.wing_profile.surface_s2( + thickness=self.panel_thickness, + front=front, + ) + def wing_r1s3_panel(self, front=True) -> Cq.Workplane: + return self.wing_profile.surface_s3( + thickness=self.panel_thickness, + front=front, ) - plane = result.faces(">Z" if front else " Cq.Assembly: @@ -582,56 +459,86 @@ class Parameters(Model): color=self.material_panel.color) .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_r1s2_panel(front=True), name="panel_front", + color=self.material_panel.color) + .constrain("panel_front", "Fixed") + .add(self.wing_r1s2_panel(front=False), name="panel_back", + color=self.material_panel.color) + # FIXME: Use s2 thickness + .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_r1s3_panel(front=True), name="panel_front", + color=self.material_panel.color) + .constrain("panel_front", "Fixed") + .add(self.wing_r1s3_panel(front=False), name="panel_back", + color=self.material_panel.color) + # FIXME: Use s2 thickness + .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + def wing_r1_assembly( + self, + parts=["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"], + ) -> Cq.Assembly: result = ( Cq.Assembly() ) - if "root" in parts: + if "s0" in parts: ( result - .add(self.wing_root(), name="root") - .constrain("root/scaffold", "Fixed") + .add(self.wing_root(), name="s0") + .constrain("s0/scaffold", "Fixed") ) if "shoulder" in parts: result.add(self.shoulder_assembly(), name="shoulder") - if "root" in parts and "shoulder" in parts: + if "s0" in parts and "shoulder" in parts: ( result - .constrain("root/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") - .constrain("root/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") - .constrain("root/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") - .constrain("root/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") + .constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") + .constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") + .constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") + .constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") ) if "s1" in parts: @@ -653,6 +560,45 @@ class Parameters(Model): "s1/shoulder_top_spacer?conn1", "Plane") ) + if "elbow" in parts: + result.add(self.elbow_assembly(), name="elbow") + + if "s2" in parts: + result.add(self.wing_r1s2_assembly(), name="s2") + + if "s1" in parts and "elbow" in parts: + ( + result + .constrain("elbow/parent_upper/top?conn1", + "s1/elbow_top_spacer?conn1", + "Plane") + .constrain("elbow/parent_upper/top?conn0", + "s1/elbow_top_spacer?conn0", + "Plane") + .constrain("elbow/parent_upper/bot?conn1", + "s1/elbow_bot_spacer?conn1", + "Plane") + .constrain("elbow/parent_upper/bot?conn0", + "s1/elbow_bot_spacer?conn0", + "Plane") + ) + + if "s2" in parts and "elbow" in parts: + ( + result + .constrain("elbow/child/bot?conn0", + "s2/elbow_bot_spacer?conn0", + "Plane") + .constrain("elbow/child/bot?conn1", + "s2/elbow_bot_spacer?conn1", + "Plane") + .constrain("elbow/child/top?conn0", + "s2/elbow_top_spacer?conn0", + "Plane") + .constrain("elbow/child/top?conn1", + "s2/elbow_top_spacer?conn1", + "Plane") + ) return result.solve() @assembly() diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/joints.py similarity index 64% rename from nhf/touhou/houjuu_nue/parts.py rename to nhf/touhou/houjuu_nue/joints.py index 600350a..6e653af 100644 --- a/nhf/touhou/houjuu_nue/parts.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -2,12 +2,209 @@ from dataclasses import dataclass, field from typing import Optional, Tuple import cadquery as Cq from nhf import Role -from nhf.build import Model, target +from nhf.build import Model, target, assembly import nhf.parts.springs as springs +from nhf.parts.joints import TorsionJoint import nhf.utils TOL = 1e-6 +@dataclass +class ShoulderJoint(Model): + + shoulder_height: float = 100.0 + torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + radius_track=18, + radius_rider=18, + groove_radius_outer=16, + groove_radius_inner=13, + track_disk_height=5.0, + rider_disk_height=5.0, + # M8 Axle + radius_axle=3.0, + # inner diameter = 9 + radius_spring=9/2 + 1.2, + spring_thickness=1.3, + spring_height=7.5, + )) + # Two holes on each side (top and bottom) are used to attach the shoulder + # joint. This governs the distance between these two holes + attach_dist: float = 25 + attach_diam: float = 8 + + + @target(name="shoulder-joint/parent") + def parent(self, + wing_root_wall_thickness: float = 5.0) -> Cq.Workplane: + joint = self.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 = 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 = ( + joint.track() + .union(lip_guard, tol=1e-6) + + # Extrude the handle + .copyWorkplane(Cq.Workplane( + 'YZ', origin=Cq.Vector((88, 0, wing_root_wall_thickness)))) + .rect(lip_width, lip_thickness, centered=(True, False)) + .extrude("next") + + # Connector holes on the lip + .copyWorkplane(Cq.Workplane( + 'YX', origin=Cq.Vector((57, 0, wing_root_wall_thickness)))) + .hole(self.attach_diam) + .moveTo(0, self.attach_dist) + .hole(self.attach_diam) + ) + result.moveTo(0, 0).tagPlane('conn0') + result.moveTo(0, self.attach_dist).tagPlane('conn1') + return result + + @property + def child_height(self) -> float: + """ + Calculates the y distance between two joint surfaces on the child side + of the shoulder joint. + """ + joint = self.torsion_joint + return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height + + @target(name="shoulder-joint/child") + def child(self, + lip_height: float = 20.0, + hole_dist: float = 10.0, + spacer_hole_diam: float = 8.0) -> Cq.Assembly: + """ + Creates the top/bottom shoulder child joint + """ + + joint = self.torsion_joint + # Half of the height of the bridging cylinder + dh = self.shoulder_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') + .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_thickness = joint.rider_disk_height + lip_ext = 40 + joint.radius_rider + 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_dist / 2 + for i in range(2): + x = hole_x - i * hole_dist + lip = lip.moveTo(x, 0).hole(spacer_hole_diam) + for i in range(2): + x = hole_x - i * hole_dist + ( + lip + .moveTo(x, 0) + .tagPlane(f"conn{1 - i}") + ) + + loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) + result = ( + Cq.Assembly() + .add(core, name="core", loc=Cq.Location()) + .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(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 assembly(self, + wing_root_wall_thickness: float = 5.0, + lip_height: float = 5.0, + hole_dist: float = 10.0, + spacer_hole_diam: float = 8.0 + ) -> Cq.Assembly: + directrix = 0 + result = ( + Cq.Assembly() + .add(self.child(lip_height=lip_height, + hole_dist=hole_dist, + spacer_hole_diam=spacer_hole_diam), + name="child", + color=Role.CHILD.color) + .constrain("child/core", "Fixed") + .add(self.torsion_joint.spring(), name="spring_top", + color=Role.DAMPING.color) + .add(self.parent(wing_root_wall_thickness), + name="parent_top", + color=Role.PARENT.color) + .add(self.torsion_joint.spring(), name="spring_bot", + color=Role.DAMPING.color) + .add(self.parent(wing_root_wall_thickness), + 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() + + @dataclass class Beam: """ @@ -69,6 +266,7 @@ class Beam: ) return result + @dataclass class DiskJoint(Model): """ @@ -325,6 +523,7 @@ class DiskJoint(Model): ) return result.solve() + @dataclass class ElbowJoint: """ @@ -375,10 +574,10 @@ class ElbowJoint: ) return result - def parent_joint_bot(self) -> Cq.Workplane: + def parent_joint_lower(self) -> Cq.Workplane: return self.disk_joint.housing_lower() - def parent_joint_top(self): + def parent_joint_upper(self): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz conn_h = self.parent_beam.spine_thickness @@ -396,12 +595,11 @@ class ElbowJoint: ) housing = self.disk_joint.housing_upper() result = ( - Cq.Assembly() + self.parent_beam.beam() .add(housing, name="housing", loc=axial_offset * Cq.Location((0, 0, housing_dz))) .add(connector, name="connector", loc=axial_offset) - .add(self.parent_beam.beam(), name="beam") ) return result @@ -410,19 +608,21 @@ class ElbowJoint: result = ( Cq.Assembly() .add(self.child_joint(), name="child", color=Role.CHILD.color) - .add(self.parent_joint_bot(), name="parent_bot", color=Role.CASING.color) - .add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color) - .constrain("parent_bot", "Fixed") + .add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color) + .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color) + .constrain("parent_lower", "Fixed") ) self.disk_joint.add_constraints( result, - housing_lower="parent_bot", - housing_upper="parent_top/housing", + housing_lower="parent_lower", + housing_upper="parent_upper/housing", disk="child/disk", angle=(0, 0, angle + da), ) return result.solve() if __name__ == '__main__': + p = ShoulderJoint() + p.build_all() p = DiskJoint() p.build_all() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index c5a9d27..cf839a7 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,21 +1,25 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M -import nhf.touhou.houjuu_nue.parts as MP +import nhf.touhou.houjuu_nue.joints as MJ from nhf.checks import pairwise_intersection -class TestDiskJoint(unittest.TestCase): +class TestJoints(unittest.TestCase): - def test_collision_0(self): - j = MP.DiskJoint() + def test_shoulder_collision_0(self): + j = MJ.ShoulderJoint() + assembly = j.assembly() + self.assertEqual(pairwise_intersection(assembly), []) + def test_disk_collision_0(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=0) self.assertEqual(pairwise_intersection(assembly), []) - def test_collision_mid(self): - j = MP.DiskJoint() + def test_disk_collision_mid(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=j.movement_angle / 2) self.assertEqual(pairwise_intersection(assembly), []) - def test_collision_max(self): - j = MP.DiskJoint() + def test_disk_collision_max(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=j.movement_angle) self.assertEqual(pairwise_intersection(assembly), []) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d39ca76..b3bc434 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -198,7 +198,7 @@ def wing_root(joint: HirthJoint, for sign in [False, True]: y = conn_height / 2 - wall_thickness side = "bottom" if sign else "top" - y = y if sign else -y + y = -y if sign else y plane = ( result # Create connector holes @@ -211,7 +211,7 @@ def wing_root(joint: HirthJoint, side = "bot" for i, (px, py) in enumerate(attach_points): tag = f"conn_{side}{i}" - plane.moveTo(px, -py if side == "top" else py).tagPlane(tag) + plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z") result.faces("X").tag("conn") @@ -430,7 +430,7 @@ class WingProfile: self.elbow_angle + 90), ("elbow_top", self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), - self.elbow_angle + 270), + self.elbow_angle - 90), ] h = (self.wrist_height - wrist_joint_parent_height) / 2 tags_wrist = [ @@ -439,7 +439,7 @@ class WingProfile: self.wrist_angle + 90), ("wrist_top", self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), - self.wrist_angle + 270), + self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -465,7 +465,7 @@ class WingProfile: self.elbow_angle + 90), ("wrist_top", self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), - self.elbow_angle + 270), + self.elbow_angle - 90), ] profile = self.profile_s3() return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) diff --git a/nhf/utils.py b/nhf/utils.py index 6d1dc8b..f75c8ec 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -7,6 +7,7 @@ Adds the functions to `Cq.Workplane`: """ import math import cadquery as Cq +from nhf import Role from typing import Union, Tuple @@ -19,7 +20,6 @@ def tagPoint(self, tag: str): Cq.Workplane.tagPoint = tagPoint - def tagPlane(self, tag: str, direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'): """ @@ -52,6 +52,57 @@ def tagPlane(self, tag: str, Cq.Workplane.tagPlane = tagPlane +def make_sphere(r: float = 2) -> Cq.Solid: + """ + Makes a full sphere. The default function makes a hemisphere + """ + return Cq.Solid.makeSphere(r, angleDegrees1=-90) +def make_arrow(size: float = 2) -> Cq.Workplane: + cone = Cq.Solid.makeCone( + radius1 = size, + radius2 = 0, + height=size) + result = ( + Cq.Workplane("XY") + .cylinder(radius=size / 2, height=size, centered=(True, True, False)) + .union(cone.located(Cq.Location((0, 0, size)))) + ) + result.faces(" Cq.Assembly: + """ + Adds a marker to make a point visible + """ + name = f"{tag}_marker" + return ( + self + .add(make_sphere(size), name=name, color=color) + .constrain(tag, name, "Point") + ) + +Cq.Assembly.markPoint = mark_point + +def mark_plane(self: Cq.Assembly, + tag: str, + size: float = 2, + color: Cq.Color = Role.MARKER.color) -> Cq.Assembly: + """ + Adds a marker to make a plane visible + """ + name = tag.replace("?", "__") + "_marker" + return ( + self + .add(make_arrow(size), name=name, color=color) + .constrain(tag, f"{name}?dir_rev", "Plane", param=180) + ) + +Cq.Assembly.markPlane = mark_plane + + def extrude_with_markers(sketch: Cq.Sketch, thickness: float, tags: list[Tuple[str, Tuple[float, float], float]], @@ -73,7 +124,7 @@ def extrude_with_markers(sketch: Cq.Sketch, ) plane = result.faces("Z").workplane() sign = -1 if reverse else 1 - for tag, (px, py), angle in tag: + for tag, (px, py), angle in tags: theta = sign * math.radians(angle) direction = (math.cos(theta), math.sin(theta), 0) plane.moveTo(px, sign * py).tagPlane(tag)