diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 78d7cfc..eb251f7 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -122,5 +122,5 @@ class HexNut(Item): ) result.faces("Z").tag("top") - result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dirX", direction="+X") return result diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f2e779b..3a414cf 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -77,22 +77,15 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") - self.wing_r1.base_joint = self.harness.hs_hirth_joint - self.wing_r2.base_joint = self.harness.hs_hirth_joint - self.wing_r3.base_joint = self.harness.hs_hirth_joint - self.wing_l1.base_joint = self.harness.hs_hirth_joint - self.wing_l2.base_joint = self.harness.hs_hirth_joint - self.wing_l3.base_joint = self.harness.hs_hirth_joint + self.wing_r1.root_joint = self.harness.root_joint + self.wing_r2.root_joint = self.harness.root_joint + self.wing_r3.root_joint = self.harness.root_joint + self.wing_l1.root_joint = self.harness.root_joint + self.wing_l2.root_joint = self.harness.root_joint + self.wing_l3.root_joint = self.harness.root_joint self.wing_r1.shoulder_joint.torsion_joint - assert self.wing_r1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_r2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_r3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - @submodel(name="harness") def submodel_harness(self) -> Model: return self.harness @@ -126,19 +119,20 @@ class Parameters(Model): result = ( Cq.Assembly() .add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0))) - .add(self.wing_r1.assembly(parts, **kwargs), name="wing_r1") - .add(self.wing_r2.assembly(parts, **kwargs), name="wing_r2") - .add(self.wing_r3.assembly(parts, **kwargs), name="wing_r3") - .add(self.wing_l1.assembly(parts, **kwargs), name="wing_l1") - .add(self.wing_l2.assembly(parts, **kwargs), name="wing_l2") - .add(self.wing_l3.assembly(parts, **kwargs), name="wing_l3") + .add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1") + .add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2") + .add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3") + .add(self.wing_l1.assembly(parts, root_offset=7, **kwargs), name="wing_l1") + .add(self.wing_l2.assembly(parts, root_offset=8, **kwargs), name="wing_l2") + .add(self.wing_l3.assembly(parts, root_offset=9, **kwargs), name="wing_l3") ) - for tag, offset in [("r1", 9), ("r2", 7), ("r3", 6), ("l1", 7), ("l2", 8), ("l3", 9)]: - self.harness.hs_hirth_joint.add_constraints( + for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]: + self.harness.add_root_joint_constraint( result, - f"harness/{tag}", - f"wing_{tag}/s0/hs", - offset=offset) + "harness/base", + f"wing_{tag}/root", + tag + ) return result.solve() @submodel(name="trident") diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index f5921e1..8507873 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -2,7 +2,8 @@ from dataclasses import dataclass, field import cadquery as Cq from nhf.parts.joints import HirthJoint from nhf import Material, Role -from nhf.build import Model, TargetKind, target, assembly +from nhf.build import Model, TargetKind, target, assembly, submodel +from nhf.touhou.houjuu_nue.joints import RootJoint import nhf.utils @dataclass(frozen=True, kw_only=True) @@ -63,33 +64,18 @@ class Harness(Model): ("r3", BASE_POS_X, -BASE_POS_Y), ("l3", -BASE_POS_X, -BASE_POS_Y), ]) - # Holes drilled onto harness for attachment with HS joint - harness_to_root_conn_diam: float = 6 - hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=25.0, - radius_inner=15.0, - tooth_height=7.0, - base_height=5.0, - n_tooth=24, - )) - - hs_joint_base_width: float = 85 - hs_joint_base_thickness: float = 10 - hs_joint_corner_fillet: float = 5 - hs_joint_corner_cbore_diam: float = 12 - hs_joint_corner_cbore_depth: float = 2 - hs_joint_corner_inset: float = 12 - - hs_joint_axis_diam: float = 12.0 - hs_joint_axis_cbore_diam: float = 20 - hs_joint_axis_cbore_depth: float = 3 + root_joint: RootJoint = field(default_factory=lambda: RootJoint()) mannequin: Mannequin = Mannequin() def __post_init__(self): super().__init__(name="harness") + @submodel(name="root-joint") + def submodel_root_joint(self) -> Model: + return self.root_joint + @target(name="profile", kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: """ @@ -115,12 +101,12 @@ class Harness(Model): .fillet(self.fillet) ) for tag, x, y in self.wing_base_pos: - conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()] sketch = ( sketch .push(conn) .tag(tag) - .circle(self.harness_to_root_conn_diam / 2, mode='s') + .circle(self.root_joint.corner_hole_diam / 2, mode='s') .reset() ) return sketch @@ -138,78 +124,27 @@ class Harness(Model): plane = result.faces(">Y").workplane() for tag, x, y in self.wing_base_pos: conn = [(px + x, py + y) for px, py - in self.hs_joint_harness_conn()] + in self.root_joint.corner_pos()] for i, (px, py) in enumerate(conn): plane.moveTo(px, py).tagPoint(f"{tag}_{i}") return result - def hs_joint_harness_conn(self) -> list[tuple[int, int]]: - """ - Generates a set of points corresponding to the connectorss - """ - dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset - return [ - (dx, dx), - (dx, -dx), - (-dx, -dx), - (-dx, dx), - ] + def add_root_joint_constraint( + self, + a: Cq.Assembly, + harness_tag: str, + joint_tag: str, + mount_tag: str): + for i in range(4): + a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point") - @target(name="hs-joint-parent") - def hs_joint_parent(self): - """ - Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth - coupling, a cylindrical base, and a mounting base. - """ - hirth = self.hs_hirth_joint.generate() - conn = self.hs_joint_harness_conn() - result = ( - Cq.Workplane('XY') - .box( - self.hs_joint_base_width, - self.hs_joint_base_width, - self.hs_joint_base_thickness, - centered=(True, True, False)) - .translate((0, 0, -self.hs_joint_base_thickness)) - .edges("|Z") - .fillet(self.hs_joint_corner_fillet) - .faces(">Z") - .workplane() - .pushPoints(conn) - .cboreHole( - diameter=self.harness_to_root_conn_diam, - cboreDiameter=self.hs_joint_corner_cbore_diam, - cboreDepth=self.hs_joint_corner_cbore_depth) - ) - # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) - - for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPoint(f"h{i}") - result = ( - result - .faces(">Z") - .workplane() - .union(hirth, tol=0.1) - .clean() - ) - result = ( - result.faces(" Cq.Assembly: + def assembly(self, with_root_joint: bool = False) -> Cq.Assembly: harness = self.surface() mannequin_z = self.mannequin.shoulder_to_waist * 0.6 + + result = ( Cq.Assembly() .addS( @@ -224,13 +159,12 @@ class Harness(Model): loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180)) .constrain("mannequin", "Fixed") ) - for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: - j = self.hs_joint_parent() - result.addS( - j, name=name, - role=Role.PARENT, - material=Material.PLASTIC_PLA) - for i in range(4): - result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") + if with_root_joint: + for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: + result.addS( + self.root_joint.assembly(), name=name, + role=Role.PARENT, + material=Material.PLASTIC_PLA) + self.add_root_joint_constraint(result, "base", name, name) result.solve() return result diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 04291aa..0dd446a 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -5,13 +5,204 @@ import cadquery as Cq from nhf import Material, Role from nhf.build import Model, target, assembly from nhf.parts.springs import TorsionSpring -from nhf.parts.fasteners import FlatHeadBolt -from nhf.parts.joints import TorsionJoint +from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob +from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes import nhf.utils TOL = 1e-6 +@dataclass +class RootJoint(Model): + """ + The Houjuu-Scarlett Mechanism + """ + knob: ThreaddedKnob = ThreaddedKnob( + mass=float('nan'), + diam_thread=12.0, + height_thread=30.0, + diam_knob=50.0, + # FIXME: Undetermined + diam_neck=30.0, + height_neck=10.0, + height_knob=10.0, + ) + hex_nut: HexNut = HexNut( + # FIXME: Undetermined + mass=float('nan'), + + diam_thread=12.0, + pitch=1.75, + thickness=9.8, + width=18.9, + ) + hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( + radius=25.0, + radius_inner=15.0, + tooth_height=7.0, + base_height=5.0, + n_tooth=24, + )) + parent_width: float = 85 + parent_thickness: float = 10 + parent_corner_fillet: float = 5 + parent_corner_cbore_diam: float = 12 + parent_corner_cbore_depth: float = 2 + parent_corner_inset: float = 12 + parent_mount_thickness: float = 25.4 / 16 + + child_corner_dx: float = 17.0 + child_corner_dz: float = 24.0 + + axis_diam: float = 12.0 + axis_cbore_diam: float = 20 + axis_cbore_depth: float = 3 + corner_hole_diam: float = 6.0 + + child_height: float = 60.0 + child_width: float = 50.0 + child_mount_thickness: float = 25.4 / 8 + + def corner_pos(self) -> list[tuple[int, int]]: + """ + Generates a set of points corresponding to the connectorss + """ + dx = self.parent_width / 2 - self.parent_corner_inset + return [ + (dx, dx), + (dx, -dx), + (-dx, -dx), + (-dx, dx), + ] + + @property + def total_height(self) -> float: + return self.parent_thickness + self.hirth_joint.total_height + + @target(name="parent") + def parent(self): + """ + Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth + coupling, a cylindrical base, and a mounting base. + """ + hirth = self.hirth_joint.generate() + conn = self.corner_pos() + result = ( + Cq.Workplane('XY') + .box( + self.parent_width, + self.parent_width, + self.parent_thickness, + centered=(True, True, False)) + .translate((0, 0, -self.parent_thickness)) + .edges("|Z") + .fillet(self.parent_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .cboreHole( + diameter=self.corner_hole_diam, + cboreDiameter=self.parent_corner_cbore_diam, + cboreDepth=self.parent_corner_cbore_depth) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.parent_thickness) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPoint(f"h{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Workplane: + hirth = self.hirth_joint.generate(is_mated=True) + dy = self.child_corner_dx + dx = self.child_corner_dz + conn = [ + (-dx, -dy), + (dx, -dy), + (dx, dy), + (-dx, dy), + ] + result = ( + Cq.Workplane('XY') + .box( + self.child_height, + self.child_width, + self.hirth_joint.base_height, + centered=(True, True, False)) + #.translate((0, 0, -self.base_joint.base_height)) + #.edges("|Z") + #.fillet(self.hs_joint_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .hole(self.corner_hole_diam) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.hirth_joint.base_height) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPlane(f"conn{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Assembly: + """ + Specify knob position to determine the position of the knob from fully + inserted (0) or fully uninserted (1) + """ + knob_h = self.total_height + self.child_mount_thickness + result = ( + Cq.Assembly() + .addS(self.parent(), name="parent", + material=Material.PLASTIC_PLA, + role=Role.PARENT) + .constrain("parent", "Fixed") + .addS(self.child(), name="child", + material=Material.PLASTIC_PLA, + role=Role.CHILD) + .addS(self.hex_nut.assembly(), name="hex_nut") + .addS(self.knob.assembly(), name="knob", + loc=Cq.Location((0, 0, knob_h * fastener_pos))) + .constrain("knob/thread", "Fixed") + .constrain("hex_nut?bot", "parent?base", "Plane", param=0) + .constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0) + ) + self.hirth_joint.add_constraints( + result, + "parent", + "child", + offset=offset + ) + return result.solve() + @dataclass class ShoulderJoint(Model): @@ -228,10 +419,11 @@ class ShoulderJoint(Model): return result @assembly() - def assembly(self, deflection: float = 0) -> Cq.Assembly: + def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly: directrix = self.directrix_id mat = Material.RESIN_TRANSPERENT mat_spring = Material.STEEL_SPRING + bolt_z = self.height / 2 + self.bolt.height_thread * (fastener_pos - 1) result = ( Cq.Assembly() .addS(self.child(), name="child", @@ -239,18 +431,21 @@ class ShoulderJoint(Model): .constrain("child/core", "Fixed") .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) - .addS(self.bolt.assembly(), name="bolt_top") .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) - .addS(self.bolt.assembly(), name="bolt_bot") .addS(self.parent_bot(), name="parent_bot", role=Role.PARENT, material=mat) - .constrain("bolt_top/thread?root", "parent_top/track?bot", "Plane", param=0) - .constrain("bolt_bot/thread?root", "parent_bot/track?bot", "Plane", param=0) + # Fasteners + .addS(self.bolt.assembly(), name="bolt_top", + loc=Cq.Location((0, 0, bolt_z))) + .constrain("bolt_top/thread?root", 'Fixed') + .addS(self.bolt.assembly(), name="bolt_bot", + loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180)) + .constrain("bolt_bot/thread?root", 'Fixed') ) TorsionJoint.add_constraints( result, diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 77f51f8..515245f 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -11,7 +11,7 @@ from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly, submodel 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, DiskJoint +from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint from nhf.parts.planar import extrude_with_markers import nhf.utils @@ -20,19 +20,8 @@ class WingProfile(Model): name: str = "wing" - base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=25.0, - radius_inner=15.0, - tooth_height=7.0, - base_height=5, - n_tooth=24, - )) + root_joint: RootJoint = field(default_factory=lambda: RootJoint()) base_width: float = 80.0 - hs_joint_corner_dx: float = 17.0 - hs_joint_corner_dz: float = 24.0 - hs_joint_corner_hole_diam: float = 6.0 - hs_joint_axis_diam: float = 12.0 - base_plate_width: float = 50.0 panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 @@ -108,6 +97,8 @@ class WingProfile(Model): self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias) + assert self.spacer_thickness == self.root_joint.child_mount_thickness + @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @@ -126,55 +117,6 @@ class WingProfile(Model): def shoulder_height(self) -> float: return self.shoulder_joint.height - @target(name="base-hs-joint") - def base_hs_joint(self) -> Cq.Workplane: - """ - Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth - coupling, a cylindrical base, and a mounting base. - """ - hirth = self.base_joint.generate(is_mated=True) - dy = self.hs_joint_corner_dx - dx = self.hs_joint_corner_dz - conn = [ - (-dx, -dy), - (dx, -dy), - (dx, dy), - (-dx, dy), - ] - result = ( - Cq.Workplane('XY') - .box( - self.root_height, - self.base_plate_width, - self.base_joint.base_height, - centered=(True, True, False)) - #.translate((0, 0, -self.base_joint.base_height)) - #.edges("|Z") - #.fillet(self.hs_joint_corner_fillet) - .faces(">Z") - .workplane() - .pushPoints(conn) - .hole(self.hs_joint_corner_hole_diam) - ) - # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height) - - for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPlane(f"conn{i}") - result = ( - result - .faces(">Z") - .workplane() - .union(hirth, tol=0.1) - .clean() - ) - result = ( - result.faces(" Cq.Sketch: """ The outer boundary of s0, used to produce the curved panel and the @@ -275,11 +217,11 @@ class WingProfile(Model): """ Should be cut """ - assert self.base_plate_width < self.base_width - assert self.hs_joint_corner_dx * 2 < self.base_width - assert self.hs_joint_corner_dz * 2 < self.root_height - dy = self.hs_joint_corner_dx - dx = self.hs_joint_corner_dz + assert self.root_joint.child_width < self.base_width + assert self.root_joint.child_corner_dx * 2 < self.base_width + assert self.root_joint.child_corner_dz * 2 < self.root_height + dy = self.root_joint.child_corner_dx + dx = self.root_joint.child_corner_dz holes = [ Hole(x=-dx, y=-dy), Hole(x=dx, y=-dy), @@ -288,17 +230,17 @@ class WingProfile(Model): ] return MountingBox( length=self.root_height, - width=self.base_plate_width, + width=self.root_joint.child_width, thickness=self.spacer_thickness, holes=holes, - hole_diam=self.hs_joint_corner_hole_diam, + hole_diam=self.root_joint.corner_hole_diam, centred=(True, True), flip_y=self.flip, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: - base_dx = -(self.base_width - self.base_plate_width) / 2 - base_dy = self.base_joint.joint_height + base_dx = -(self.base_width - self.root_joint.child_width) / 2 + base_dy = self.root_joint.hirth_joint.joint_height loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) tags = [ ("shoulder", @@ -351,14 +293,6 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) - hs_joint = self.base_hs_joint() - ( - result - .addS(hs_joint, name="hs", role=Role.CHILD, material=self.mat_hs_joint) - .constrain("hs?conn0", "base?conn0", "Plane", param=0) - .constrain("hs?conn1", "base?conn1", "Plane", param=0) - .constrain("hs?conn2", "base?conn2", "Plane", param=0) - ) return result.solve() @@ -627,9 +561,11 @@ class WingProfile(Model): parts: Optional[list[str]] = None, shoulder_deflection: float = 0.0, elbow_wrist_deflection: float = 0.0, + root_offset: int = 5, + fastener_pos: float = 0.0, ) -> Cq.Assembly(): if parts is None: - parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] + parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( Cq.Assembly() ) @@ -639,8 +575,23 @@ class WingProfile(Model): if "s0" in parts: result.add(self.assembly_s0(), name="s0") + if "root" in parts: + result.addS(self.root_joint.assembly( + offset=root_offset, + fastener_pos=fastener_pos, + ), name="root") + result.constrain("root/parent", "Fixed") + if "s0" in parts and "root" in parts: + ( + result + .constrain("s0/base?conn0", "root/child?conn0", "Plane", param=0) + .constrain("s0/base?conn1", "root/child?conn1", "Plane", param=0) + .constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0) + ) if "shoulder" in parts: - result.add(self.shoulder_joint.assembly(deflection=shoulder_deflection * 80), name="shoulder") + result.add(self.shoulder_joint.assembly( + fastener_pos=fastener_pos, + deflection=shoulder_deflection * 80), name="shoulder") if "s0" in parts and "shoulder" in parts: ( result