diff --git a/nhf/parts/box.py b/nhf/parts/box.py new file mode 100644 index 0000000..a95f512 --- /dev/null +++ b/nhf/parts/box.py @@ -0,0 +1,110 @@ +import cadquery as Cq +from dataclasses import dataclass, field +from typing import Tuple, Optional, Union +import nhf.utils + +def box_with_centre_holes( + length: float, + width: float, + height: float, + hole_loc: list[float], + hole_diam: float = 6.0, + ) -> Cq.Workplane: + """ + Creates a box with holes along the X axis, marked `conn0, conn1, ...`. The + box's y axis is centred + """ + result = ( + Cq.Workplane('XY') + .box(length, width, height, centered=(False, True, False)) + .faces(">Z") + .workplane() + ) + plane = result + for i, x in enumerate(hole_loc): + result = result.moveTo(x, 0).hole(hole_diam) + plane.moveTo(x, 0).tagPlane(f"conn{i}") + return result + +@dataclass +class Hole: + x: float + y: float = 0.0 + diam: Optional[float] = None + tag: Optional[str] = None + +@dataclass +class MountingBox: + """ + Create a box with marked holes + """ + length: float = 100.0 + width: float = 60.0 + thickness: float = 1.0 + + # List of (x, y), diam + holes: list[Hole] = field(default_factory=lambda: [ + Hole(x=5, y=5, diam=3), + Hole(x=20, y=10, diam=5), + ]) + hole_diam: Optional[float] = None + + centred: Tuple[bool, bool] = (False, True) + + generate_side_tags: bool = True + + def profile(self) -> Cq.Sketch: + bx, by = 0, 0 + if not self.centred[0]: + bx = self.length / 2 + if not self.centred[1]: + by = self.width / 2 + result = ( + Cq.Sketch() + .push([(bx, by)]) + .rect(self.length, self.width) + ) + for hole in self.holes: + diam = hole.diam if hole.diam else self.hole_diam + result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s') + return result + + def generate(self) -> Cq.Workplane: + """ + Creates box shape with markers + """ + result = ( + Cq.Workplane('XY') + .placeSketch(self.profile()) + .extrude(self.thickness) + ) + plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness) + for i, hole in enumerate(self.holes): + tag = hole.tag if hole.tag else f"conn{i}" + plane.moveTo(hole.x, hole.y).tagPlane(tag) + + if self.generate_side_tags: + result.faces("Z").val().Center()).tagPlane("left") + result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") + result.faces("Z").val().Center()).tagPlane("bot") + result.faces(">X").workplane(origin=result.vertices(">X and Z").val().Center()).tagPlane("top") + result.faces(">Z").tag("dir") + return result + + def marked_assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.generate(), name="box") + ) + for i in range(len(self.holes)): + result.markPlane(f"box?conn{i}") + if self.generate_side_tags: + ( + result + .markPlane("box?left") + .markPlane("box?right") + .markPlane("box?dir") + .markPlane("box?top") + .markPlane("box?bot") + ) + return result.solve() diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index bb1c449..9867b4f 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -105,6 +105,18 @@ class HirthJoint: ) return result + def add_constraints(self, + assembly: Cq.Assembly, + parent: str, + child: str, + angle: int = 0): + ( + assembly + .constrain(f"{parent}?mate", f"{child}?mate", "Plane") + .constrain(f"{parent}?dir", f"{child}?dir", + "Axis", param=angle * self.tooth_angle) + ) + def assembly(self, offset: int = 1): """ Generate an example assembly diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 844ce48..4e57fae 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -62,7 +62,11 @@ class Parameters(Model): )) wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( - shoulder_height=100.0, + shoulder_joint=MJ.ShoulderJoint( + height=100.0, + ), + elbow_joint=MJ.ElbowJoint(), + wrist_joint=MJ.ElbowJoint(), elbow_height=110.0, )) @@ -70,14 +74,6 @@ class Parameters(Model): wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - 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( - )) - """ Heights for various wing joints, where the numbers start from the first joint. @@ -109,6 +105,7 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") self.harness.hs_hirth_joint = self.hs_hirth_joint + self.wing_profile.base_joint = self.hs_hirth_joint 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, \ @@ -145,309 +142,20 @@ class Parameters(Model): def harness_assembly(self) -> Cq.Assembly: return self.harness.assembly() - #@target(name="wing/joining-plate", kind=TargetKind.DXF) - #def joining_plate(self) -> Cq.Workplane: - # return self.wing_joining_plate.plate() - - @target(name="wing/root") - def wing_root(self) -> Cq.Assembly: - """ - Generate the wing root which contains a Hirth joint at its base and a - rectangular opening on its side, with the necessary interfaces. - """ - return MW.wing_root( - joint=self.hs_hirth_joint, - 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, - ) - @target(name="wing/proto-shoulder-joint-parent", prototype=True) def proto_shoulder_joint_parent(self): - return self.shoulder_joint.torsion_joint.track() + return self.wing_profile.shoulder_joint.torsion_joint.track() @target(name="wing/proto-shoulder-joint-child", prototype=True) def proto_shoulder_joint_child(self): - return self.shoulder_joint.torsion_joint.rider() - @assembly() - 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, - ) - @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: - result = ( - Cq.Workplane('XZ') - .sketch() - .rect(self.wing_s1_spacer_width, self.wing_s1_thickness) - .finalize() - .extrude(self.wing_s1_spacer_thickness) - ) - result.faces("Z").tag("weld2") - result.faces(">Y").tag("dir") - return result - - @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) - def wing_s1_shoulder_spacer(self) -> Cq.Workplane: - """ - 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('XY') - .sketch() - .rect(length, self.wing_s1_thickness) - .push([ - (0, 0), - (dx, 0), - ]) - .circle(hole_diam / 2, mode='s') - .finalize() - .extrude(h) - ) - # Tag the mating surfaces to be glued - result.faces("Y").workplane().moveTo(-length / 2, h).tagPlane("right") - - # Tag the directrix - result.faces(">Z").tag("dir") - - # Tag the holes - plane = result.faces(">Z").workplane() - # Side closer to the parent is 0 - 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: - 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, - ) - - @assembly() - def wing_r1s1_assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.wing_r1s1_panel(front=True), name="panel_front", - color=self.material_panel.color) - .constrain("panel_front", "Fixed") - .add(self.wing_r1s1_panel(front=False), name="panel_back", - 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: - result = ( - Cq.Assembly() - ) - if "s0" in parts: - ( - result - .add(self.wing_root(), name="s0") - .constrain("s0/scaffold", "Fixed") - ) - if "shoulder" in parts: - result.add(self.shoulder_assembly(), name="shoulder") - - if "s0" in parts and "shoulder" in parts: - ( - result - .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: - result.add(self.wing_r1s1_assembly(), name="s1") - - if "s1" in parts and "shoulder" in parts: - ( - result - .constrain("shoulder/child/lip_bot?conn0", - "s1/shoulder_bot_spacer?conn0", - "Plane") - .constrain("shoulder/child/lip_bot?conn1", - "s1/shoulder_bot_spacer?conn1", - "Plane") - .constrain("shoulder/child/lip_top?conn0", - "s1/shoulder_top_spacer?conn0", - "Plane") - .constrain("shoulder/child/lip_top?conn1", - "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") - ) - if len(parts) > 1: - result.solve() - return result + def wing_r1_assembly(self) -> Cq.Assembly: + return self.wing_profile.assembly() @assembly() - def wings_assembly(self) -> Cq.Assembly: + def wings_harness_assembly(self) -> Cq.Assembly: """ Assembly of harness with all the wings """ @@ -456,18 +164,14 @@ class Parameters(Model): result = ( Cq.Assembly() .add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0))) - .add(self.wing_root(), name="w0_r1") - .add(self.wing_root(), name="w0_l1") - .constrain("harness/base", "Fixed") - .constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane") - .constrain("w0_r1/joint?dir", "harness/r1?dir", - "Axis", param=7 * a_tooth) - .constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane") - .constrain("w0_l1/joint?dir", "harness/l1?dir", - "Axis", param=-1 * a_tooth) - .solve() + .add(self.wing_r1_assembly(), name="wing_r1") + .add(self.wing_r1_assembly(), name="wing_r2") + .add(self.wing_r1_assembly(), name="wing_r3") ) - return result + self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", angle=9) + self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", angle=8) + self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", angle=7) + return result.solve() @assembly(collision_check=False) def trident_assembly(self) -> Cq.Assembly: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index dfd5207..d25c22c 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -5,6 +5,7 @@ from nhf import Role from nhf.build import Model, target, assembly import nhf.parts.springs as springs from nhf.parts.joints import TorsionJoint +from nhf.parts.box import box_with_centre_holes import nhf.utils TOL = 1e-6 @@ -12,7 +13,7 @@ TOL = 1e-6 @dataclass class ShoulderJoint(Model): - shoulder_height: float = 100.0 + height: float = 100.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( radius_track=18, radius_rider=18, @@ -27,47 +28,67 @@ class ShoulderJoint(Model): 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 + + # On the parent side, drill vertical holes + + parent_conn_hole_diam: float = 6.0 + # Position of the holes relative + parent_conn_hole_pos: list[float] = field(default_factory=lambda: [20, 30]) + + parent_lip_length: float = 40.0 + parent_lip_width: float = 20.0 + parent_lip_thickness: float = 8.0 + parent_lip_ext: float = 40.0 + parent_lip_guard_height: float = 10.0 + + # Measured from centre of axle + child_lip_length: float = 45.0 + child_lip_width: float = 20.0 + child_conn_hole_diam: float = 6.0 + # Measured from centre of axle + child_conn_hole_pos: list[float] = field(default_factory=lambda: [25, 35]) + child_core_thickness: float = 3.0 @target(name="shoulder-joint/parent") def parent(self, - wing_root_wall_thickness: float = 5.0) -> Cq.Workplane: + root_wall_thickness: float = 25.4 / 16) -> Cq.Assembly: 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 + dz = root_wall_thickness + assert self.parent_lip_width <= joint.radius_track * 2 + assert self.parent_lip_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)) + Cq.Solid.makeBox( + self.parent_lip_ext, + self.parent_lip_width, + self.parent_lip_guard_height) + .located(Cq.Location((0, -self.parent_lip_width/2 , dz))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height + dz)) ) + lip = box_with_centre_holes( + length=self.parent_lip_length - dz, + width=self.parent_lip_width, + height=self.parent_lip_thickness, + hole_loc=[ + self.height / 2 - dz - x + for x in self.parent_conn_hole_pos + ], + hole_diam=self.parent_conn_hole_diam, + ) + # Flip so the lip's holes point to -X + loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) + # so they point to +X + loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) + loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, dz)) + 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) + Cq.Assembly() + .add(joint.track(), name="track") + .add(lip_guard, name="lip_guard") + .add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis) ) - result.moveTo(0, 0).tagPlane('conn0') - result.moveTo(0, self.attach_dist).tagPlane('conn1') return result @property @@ -77,32 +98,32 @@ class ShoulderJoint(Model): of the shoulder joint. """ joint = self.torsion_joint - return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height + return self.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: + def child(self) -> Cq.Assembly: """ Creates the top/bottom shoulder child joint """ joint = self.torsion_joint + assert all(r > joint.radius_rider for r in self.child_conn_hole_pos) + assert all(r < self.child_lip_length for r in self.child_conn_hole_pos) + # Half of the height of the bridging cylinder - dh = self.shoulder_height / 2 - joint.total_height + dh = self.height / 2 - joint.total_height core_start_angle = 30 core_end_angle1 = 90 core_end_angle2 = 180 - core_thickness = 2 + radius_core_inner = joint.radius_rider - self.child_core_thickness 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') + .circle(radius_core_inner, mode='s') ) core_profile2 = ( Cq.Sketch() @@ -110,7 +131,7 @@ class ShoulderJoint(Model): .segment((0, 0)) .close() .assemble() - .circle(joint.radius_rider - core_thickness, mode='s') + .circle(radius_core_inner, mode='s') ) core = ( Cq.Workplane('XY') @@ -123,33 +144,24 @@ class ShoulderJoint(Model): .extrude(dh * 2) .translate(Cq.Vector(0, 0, -dh)) ) - # Create the upper and lower lips + assert self.child_lip_width / 2 <= joint.radius_rider 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() + lip = box_with_centre_holes( + length=self.child_lip_length, + width=self.child_lip_width, + height=lip_thickness, + hole_loc=self.child_conn_hole_pos, + hole_diam=self.child_conn_hole_diam, + ) + lip = ( + lip + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + radius=joint.radius_rider, + height=lip_thickness, + centered=(True, True, False), + combine='cut') ) - 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() @@ -167,18 +179,12 @@ class ShoulderJoint(Model): @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 + wing_root_wall_thickness: float = 25.4/16, ) -> 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", + .add(self.child(), name="child", color=Role.CHILD.color) .constrain("child/core", "Fixed") .add(self.torsion_joint.spring(), name="spring_top", @@ -194,12 +200,12 @@ class ShoulderJoint(Model): ) TorsionJoint.add_constraints(result, rider="child/rider_top", - track="parent_top", + track="parent_top/track", spring="spring_top", directrix=directrix) TorsionJoint.add_constraints(result, rider="child/rider_bot", - track="parent_bot", + track="parent_bot/track", spring="spring_bot", directrix=directrix) return result.solve() @@ -565,15 +571,31 @@ class ElbowJoint: assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing + def child_hole_pos(self) -> list[float]: + """ + List of hole positions measured from axle + """ + dx = self.child_beam.hole_dist / 2 + r = self.child_arm_radius + return [r - dx, r + dx] + def parent_hole_pos(self) -> list[float]: + """ + List of hole positions measured from axle + """ + dx = self.parent_beam.hole_dist / 2 + r = self.parent_arm_radius + return [r - dx, r + dx] + def child_joint(self) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 # We need to ensure the disk is on the "other" side so - flip = Cq.Location((0, 0, 0), (0, 0, 1), 180) + flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) + flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) result = ( self.child_beam.beam() .add(self.disk_joint.disk(), name="disk", - loc=flip * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) #.constrain("disk", "Fixed") #.constrain("top", "Fixed") #.constrain("bot", "Fixed") diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 46fd676..b832dff 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -4,341 +4,46 @@ This file describes the shapes of the wing shells. The joints are defined in """ import math from enum import Enum -from dataclasses import dataclass -from typing import Mapping, Tuple +from dataclasses import dataclass, field +from typing import Mapping, Tuple, Optional import cadquery as Cq from nhf import Material, Role +from nhf.build import Model, target, assembly +from nhf.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint +from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint import nhf.utils - -def wing_root_profiles( - base_sweep=150, - wall_thickness=8, - base_radius=40, - middle_offset=30, - middle_height=80, - conn_thickness=40, - conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: - assert base_sweep < 180 - assert middle_offset > 0 - theta = math.pi * base_sweep / 180 - c, s = math.cos(theta), math.sin(theta) - c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75) - c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2) - r1 = base_radius - r2 = base_radius - wall_thickness - base = ( - Cq.Sketch() - .arc( - (c * r1, s * r1), - (c_1 * r1, s_1 * r1), - (c_2 * r1, s_2 * r1), - ) - .arc( - (c_2 * r1, s_2 * r1), - (r1, 0), - (c_2 * r1, -s_2 * r1), - ) - .arc( - (c_2 * r1, -s_2 * r1), - (c_1 * r1, -s_1 * r1), - (c * r1, -s * r1), - ) - .segment( - (c * r1, -s * r1), - (c * r2, -s * r2), - ) - .arc( - (c * r2, -s * r2), - (c_1 * r2, -s_1 * r2), - (c_2 * r2, -s_2 * r2), - ) - .arc( - (c_2 * r2, -s_2 * r2), - (r2, 0), - (c_2 * r2, s_2 * r2), - ) - .arc( - (c_2 * r2, s_2 * r2), - (c_1 * r2, s_1 * r2), - (c * r2, s * r2), - ) - .segment( - (c * r2, s * r2), - (c * r1, s * r1), - ) - .assemble(tag="wire") - .wires().val() - ) - assert isinstance(base, Cq.Wire) - - # The interior sweep is given by theta, but the exterior sweep exceeds the - # interior sweep so the wall does not become thinner towards the edges. - # If the exterior sweep is theta', it has to satisfy - # - # sin(theta) * r2 + wall_thickness = sin(theta') * r1 - x, y = conn_thickness / 2, middle_height / 2 - t = wall_thickness - dx = middle_offset - middle = ( - Cq.Sketch() - # Interior arc, top point - .arc( - (x - t, y - t), - (x - t + dx, 0), - (x - t, -y + t), - ) - .segment( - (x - t, -y + t), - (-x, -y+t) - ) - .segment((-x, -y)) - .segment((x, -y)) - # Outer arc, bottom point - .arc( - (x, -y), - (x + dx, 0), - (x, y), - ) - .segment( - (x, y), - (-x, y) - ) - .segment((-x, y-t)) - #.segment((x2, a)) - .close() - .assemble(tag="wire") - .wires().val() - ) - assert isinstance(middle, Cq.Wire) - - x, y = conn_thickness / 2, conn_height / 2 - t = wall_thickness - tip = ( - Cq.Sketch() - .segment((-x, y), (x, y)) - .segment((x, -y)) - .segment((-x, -y)) - .segment((-x, -y+t)) - .segment((x-t, -y+t)) - .segment((x-t, y-t)) - .segment((-x, y-t)) - .close() - .assemble(tag="wire") - .wires().val() - ) - return base, middle, tip - - -def wing_root(joint: HirthJoint, - bolt_diam: int = 12, - union_tol=1e-4, - shoulder_attach_diam=8, - shoulder_attach_dist=25, - 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, 4), - (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s), - ] - root_profile, middle_profile, tip_profile = wing_root_profiles( - conn_thickness=conn_thickness, - conn_height=conn_height, - wall_thickness=8, - ) - middle_profile = middle_profile.located(Cq.Location( - (-40, 0, -40), (0, 1, 0), 30 - )) - antetip_profile = tip_profile.located(Cq.Location( - (-95, 0, -75), (0, 1, 0), 60 - )) - tip_profile = tip_profile.located(Cq.Location( - tip_centre, (0, 1, 0), 90 - )) - profiles = [ - root_profile, - middle_profile, - antetip_profile, - tip_profile, - ] - result = None - for p1, p2 in zip(profiles[:-1], profiles[1:]): - seg = ( - Cq.Workplane('XY') - .add(p1) - .toPending() - .workplane() # This call is necessary - .add(p2) - .toPending() - .loft() - ) - if result: - result = result.union(seg, tol=union_tol) - else: - result = seg - result = ( - result - # Create connector holes - .copyWorkplane( - Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0))) - ) - .pushPoints(attach_points) - .hole(shoulder_attach_diam) - ) - # Generate attach point tags - - for sign in [False, True]: - y = conn_height / 2 - wall_thickness - side = "bottom" if sign else "top" - y = -y if sign else y - plane = ( - result - # Create connector holes - .copyWorkplane( - Cq.Workplane(side, origin=tip_centre + - 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 if side == "top" else py).tagPlane(tag, "-Z") - - result.faces("X").tag("conn") - - j = ( - joint.generate(is_mated=True) - .faces(" list[Tuple[float, float]]: - """ - Generate outer wing shape spline - """ - - def profile(self) -> Cq.Sketch: - sketch = ( - Cq.Sketch() - .segment((-self.root_width, 0), (0, 0)) - .spline([ - (0, 0), - (-30.0, 80.0), - (self.tip_x, self.tip_y) - ]) - .segment( - (self.tip_x, self.tip_y), - (self.tip_x, self.tip_y - self.shoulder_width) - ) - .segment( - (self.tip_x, self.tip_y - self.shoulder_width), - (-self.root_width, 0) - ) - .assemble() - ) - return sketch - - def spacer(self) -> Cq.Workplane: - """ - Creates a rectangular spacer. This could be cut from acrylic. - - There are two holes on the top of the spacer. With the holes - """ - length = self.height - width = 10.0 - h = self.spacer_thickness - result = ( - Cq.Workplane('XY') - .sketch() - .rect(length, width) - .finalize() - .extrude(h) - ) - # Tag the mating surfaces to be glued - result.faces("X").workplane().tagPlane("right") - - # Tag the directrix - result.faces(">Z").tag("dir") - return result - - def surface(self, top: bool = False) -> Cq.Workplane: - tags = [ - ("shoulder", (self.tip_x, self.tip_y + 30), 0), - ("base", (-self.root_width, 0), 90), - ] - return nhf.utils.extrude_with_markers( - self.profile(), - self.panel_thickness, - tags, - reverse=not top, - ) - - def assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.surface(top=True), name="bot") - .add(self.surface(top=False), name="top") - .constrain("bot@faces@>Z", "top@faces@ self.ring_radius_inner self.elbow_theta = math.radians(self.elbow_angle) @@ -377,6 +88,145 @@ class WingProfile: self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y + @property + def root_height(self) -> float: + return self.shoulder_joint.height + + def profile_s0(self) -> Cq.Sketch: + tip_x = self.shoulder_tip_x + tip_y = self.shoulder_tip_y + sketch = ( + Cq.Sketch() + .segment((-self.root_width, 0), (0, 0)) + .spline([ + (0, 0), + (-30.0, 80.0), + (tip_x, tip_y) + ]) + .segment( + (tip_x, tip_y), + (tip_x, tip_y - self.shoulder_width) + ) + .segment( + (tip_x, tip_y - self.shoulder_width), + (-self.root_width, 0) + ) + .assemble() + ) + return sketch + + def spacer_s0_shoulder(self) -> MountingBox: + """ + Should be cut + """ + holes = [ + hole + for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos) + for hole in [ + Hole(x=x, tag=f"conn_top{i}"), + Hole(x=-x, tag=f"conn_bot{i}"), + ] + ] + return MountingBox( + length=self.shoulder_joint.height, + width=self.shoulder_width, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.shoulder_joint.parent_conn_hole_diam, + centred=(True, True), + ) + def spacer_s0_base(self) -> MountingBox: + """ + Should be cut + """ + dx = self.hs_joint_corner_dx + holes = [ + Hole(x=-dx, y=-dx), + Hole(x=dx, y=-dx), + Hole(x=dx, y=dx), + Hole(x=-dx, y=dx), + ] + return MountingBox( + length=self.root_height, + width=self.root_width, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.hs_joint_corner_hole_diam, + centred=(True, True), + ) + + def surface_s0(self, top: bool = False) -> Cq.Workplane: + tags = [ + ("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0), + ("base", (0, 0), 90), + ] + return nhf.utils.extrude_with_markers( + self.profile_s0(), + self.panel_thickness, + tags, + reverse=not top, + ) + + def assembly_s0(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s0(top=True), name="bot", color=self.material_panel.color) + .add(self.surface_s0(top=False), name="top", color=self.material_panel.color) + .constrain("bot@faces@>Z", "top@faces@ float: dx = self.ring_x @@ -401,10 +251,10 @@ class WingProfile: Cq.Sketch() .segment( (0, 0), - (0, self.shoulder_height), + (0, self.shoulder_joint.height), tag="shoulder") .arc( - (0, self.shoulder_height), + (0, self.shoulder_joint.height), (self.elbow_top_x, self.elbow_top_y), (self.wrist_top_x, self.wrist_top_y), tag="s1_top") @@ -475,31 +325,74 @@ class WingProfile: ) return profile def surface_s1(self, - thickness: float = 25.4/16, - shoulder_mount_inset: float = 20, - shoulder_joint_child_height: float = 80, - elbow_mount_inset: float = 20, - elbow_joint_parent_height: float = 60, + shoulder_mount_inset: float = 0, + elbow_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: - assert shoulder_joint_child_height < self.shoulder_height - assert elbow_joint_parent_height < self.elbow_height - h = (self.shoulder_height - shoulder_joint_child_height) / 2 + shoulder_h = self.shoulder_joint.child_height + h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ ("shoulder_bot", (shoulder_mount_inset, h), 90), - ("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270), + ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270), ] - h = (self.elbow_height - elbow_joint_parent_height) / 2 + elbow_h = self.elbow_joint.parent_beam.total_height + h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(-elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", - self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height), + self.elbow_to_abs(-elbow_mount_inset, h + elbow_h), self.elbow_angle + 270), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow - return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def spacer_s1_shoulder(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.shoulder_joint.child_conn_hole_pos + ] + return MountingBox( + length=50.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.shoulder_joint.child_conn_hole_diam, + ) + def spacer_s1_elbow(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.elbow_joint.parent_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.elbow_joint.hole_diam, + ) + def assembly_s1(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s1(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s1(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: @@ -513,33 +406,78 @@ class WingProfile: return profile def surface_s2(self, thickness: float = 25.4/16, - elbow_mount_inset: float = 20, - elbow_joint_child_height: float = 80, - wrist_mount_inset: float = 20, - wrist_joint_parent_height: float = 60, + elbow_mount_inset: float = 0, + wrist_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: - assert elbow_joint_child_height < self.elbow_height - h = (self.elbow_height - elbow_joint_child_height) / 2 + elbow_h = self.elbow_joint.child_beam.total_height + h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", - self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), + self.elbow_to_abs(elbow_mount_inset, h + elbow_h), self.elbow_angle - 90), ] - h = (self.wrist_height - wrist_joint_parent_height) / 2 + wrist_h = self.wrist_joint.parent_beam.total_height + h = (self.wrist_height - wrist_h) / 2 tags_wrist = [ ("wrist_bot", self.wrist_to_abs(-wrist_mount_inset, h), self.wrist_angle + 90), ("wrist_top", - self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), + self.wrist_to_abs(-wrist_mount_inset, h + wrist_h), self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + def spacer_s2_elbow(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.elbow_joint.child_hole_pos() + ] + return MountingBox( + length=50.0, # FIXME: magic + width=self.s2_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.elbow_joint.hole_diam, + ) + def spacer_s2_wrist(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.wrist_joint.parent_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.wrist_joint.hole_diam, + ) + def assembly_s2(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s2(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s2(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( @@ -549,61 +487,127 @@ class WingProfile: ) return profile def surface_s3(self, - thickness: float = 25.4/16, - wrist_mount_inset: float = 20, - wrist_joint_child_height: float = 80, front: bool = True) -> Cq.Workplane: - assert wrist_joint_child_height < self.wrist_height - h = (self.wrist_height - wrist_joint_child_height) / 2 + wrist_mount_inset = 0 + wrist_h = self.wrist_joint.child_beam.total_height + h = (self.wrist_height - wrist_h) / 2 tags = [ ("wrist_bot", - self.elbow_to_abs(wrist_mount_inset, h), - self.elbow_angle + 90), + self.wrist_to_abs(wrist_mount_inset, h), + self.wrist_angle + 90), ("wrist_top", - self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), - self.elbow_angle - 90), + self.wrist_to_abs(wrist_mount_inset, h + wrist_h), + self.wrist_angle - 90), ] profile = self.profile_s3() - return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) - - - def wing_r1s1_profile(self) -> Cq.Sketch: - """ - Generates the first wing segment profile, with the wing root pointing in - the positive x axis. - """ - - - w = 270 - # Depression of the wing middle, measured - h = 0 - # spline curve easing extension - theta = math.radians(30) - c_th, s_th = math.cos(theta), math.sin(theta) - bend = 30 - ext = 40 - ext_dh = -5 - assert ext * 2 < w - - factor = 0.7 - - result = ( - Cq.Sketch() - .segment((0, 0), (0, self.shoulder_height)) - .spline([ - (0, self.shoulder_height), - ((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend), - (w - s_th * self.elbow_height, self.elbow_height * c_th - h), - ]) - .segment( - (w - s_th * self.elbow_height, self.elbow_height * c_th -h), - (w, -h), - ) - .spline([ - (0, 0), - (w / 2, -h / 2 - bend), - (w, -h), - ]) - .assemble() + return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def spacer_s3_wrist(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.wrist_joint.child_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.wrist_joint.hole_diam ) - return result + def assembly_s3(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s3(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s3(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Assembly(): + if parts is None: + parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] + result = ( + Cq.Assembly() + ) + if "s0" in parts: + result.add(self.assembly_s0(), name="s0") + if "shoulder" in parts: + result.add(self.shoulder_joint.assembly(), name="shoulder") + if "s0" in parts and "shoulder" in parts: + ( + result + .constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane") + .constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") + .constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane") + .constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") + ) + if "s1" in parts: + result.add(self.assembly_s1(), name="s1") + if "s1" in parts and "shoulder" in parts: + ( + result + .constrain("s1/shoulder_top?conn0", "shoulder/child/lip_top?conn0", "Plane") + .constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane") + .constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane") + .constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane") + ) + if "elbow" in parts: + result.add(self.elbow_joint.assembly(), name="elbow") + if "s1" in parts and "elbow" in parts: + ( + result + .constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane") + .constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane") + .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane") + .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane") + ) + if "s2" in parts: + result.add(self.assembly_s2(), name="s2") + if "s2" in parts and "elbow" in parts: + ( + result + .constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane") + .constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane") + .constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane") + .constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane") + ) + if "wrist" in parts: + result.add(self.wrist_joint.assembly(), name="wrist") + if "s2" in parts and "wrist" in parts: + ( + result + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") + ) + if "s3" in parts: + result.add(self.assembly_s3(), name="s3") + if "s3" in parts and "wrist" in parts: + ( + result + .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") + ) + + return result.solve() + + +