From f5b048d0b98efa06c43ca675d677b73d0da5e776 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 21:00:10 -0700 Subject: [PATCH] feat: Add linear actuator component --- nhf/materials.py | 4 +- nhf/parts/fasteners.py | 7 +- nhf/parts/item.py | 6 +- nhf/parts/joints.py | 2 +- nhf/touhou/houjuu_nue/electronics.py | 155 +++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/joints.py | 104 ++++++++++-------- 6 files changed, 223 insertions(+), 55 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/electronics.py diff --git a/nhf/materials.py b/nhf/materials.py index af336da..bc172c3 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -31,6 +31,7 @@ class Role(Flag): STRUCTURE = auto() DECORATION = auto() ELECTRONIC = auto() + MOTION = auto() # Fasteners, etc. CONNECTION = auto() @@ -61,7 +62,8 @@ ROLE_COLOR_MAP = { Role.DAMPING: _color('springgreen', 1.0), Role.STRUCTURE: _color('gray', 0.4), Role.DECORATION: _color('lightseagreen', 0.4), - Role.ELECTRONIC: _color('mediumorchid', 0.5), + Role.ELECTRONIC: _color('mediumorchid', 0.7), + Role.MOTION: _color('thistle3', 0.7), Role.CONNECTION: _color('steelblue3', 0.8), Role.HANDLE: _color('tomato4', 0.8), } diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index c9c9e61..38c8f84 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -42,10 +42,7 @@ class FlatHeadBolt(Item): @dataclass(frozen=True) class ThreaddedKnob(Item): """ - Sourced from: - - > Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening - > Screws + A threaded rod with knob on one side """ diam_thread: float height_thread: float @@ -104,7 +101,7 @@ class HexNut(Item): return f"HexNut M{int(self.diam_thread)}-{self.pitch}" @property - def role(self): + def role(self) -> Role: return Role.CONNECTION def generate(self) -> Cq.Workplane: diff --git a/nhf/parts/item.py b/nhf/parts/item.py index 1e321fc..e7a153e 100644 --- a/nhf/parts/item.py +++ b/nhf/parts/item.py @@ -44,9 +44,9 @@ class Item: a = self.generate(**kwargs) if isinstance(a, Cq.Workplane): a = Cq.Assembly(a) - if role := self.role: - a.metadata[KEY_ROLE] = role - a.color = role.color_avg() + if role := self.role: + a.metadata[KEY_ROLE] = role + a.color = role.color_avg() assert isinstance(a, Cq.Assembly) assert KEY_ITEM not in a.metadata a.metadata[KEY_ITEM] = self diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 31bb686..e0e0357 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -393,7 +393,7 @@ class TorsionJoint: def rider_track_assembly(self, directrix: int = 0, deflection: float = 0): rider = self.rider() track = self.track() - spring = self.spring.generate(deflection=deflection) + spring = self.spring.assembly(deflection=deflection) result = ( Cq.Assembly() .addS(spring, name="spring", role=Role.DAMPING) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py new file mode 100644 index 0000000..5e4f690 --- /dev/null +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -0,0 +1,155 @@ +""" +Electronic components +""" +from dataclasses import dataclass +import cadquery as Cq +from nhf.materials import Role +from nhf.parts.item import Item +from nhf.parts.fasteners import FlatHeadBolt, HexNut + +@dataclass(frozen=True) +class LinearActuator(Item): + stroke_length: float + shaft_diam: float = 9.04 + + front_hole_ext: float = 4.41 + front_hole_diam: float = 4.41 + front_length: float = 9.55 + front_width: float = 9.24 + front_height: float = 5.98 + + segment1_length: float = 37.55 + segment1_width: float = 15.95 + segment1_height: float = 11.94 + + segment2_length: float = 37.47 + segment2_width: float = 20.03 + segment2_height: float = 15.03 + + back_hole_ext: float = 4.58 + back_hole_diam: float = 4.18 + back_length: float = 9.27 + back_width: float = 10.16 + back_height: float = 8.12 + + @property + def name(self) -> str: + return f"LinearActuator {self.stroke_length}mm" + + @property + def role(self) -> Role: + return Role.MOTION + + @property + def conn_length(self): + return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext + + def generate(self, pos: float=0) -> Cq.Assembly: + stroke_x = pos * self.stroke_length + front = ( + Cq.Workplane('XZ') + .cylinder( + radius=self.front_width / 2, + height=self.front_height, + centered=True, + ) + .box( + length=self.front_hole_ext, + width=self.front_width, + height=self.front_height, + combine=True, + centered=(False, True, True) + ) + .copyWorkplane(Cq.Workplane('XZ')) + .cylinder( + radius=self.front_hole_diam / 2, + height=self.front_height, + centered=True, + combine='cut', + ) + ) + if stroke_x > 0: + shaft = ( + Cq.Workplane('YZ') + .cylinder( + radius=self.shaft_diam / 2, + height=stroke_x, + centered=(True, True, False) + ) + ) + else: + shaft = None + segment1 = ( + Cq.Workplane() + .box( + length=self.segment1_length, + height=self.segment1_width, + width=self.segment1_height, + centered=(False, True, True), + ) + ) + segment2 = ( + Cq.Workplane() + .box( + length=self.segment2_length, + height=self.segment2_width, + width=self.segment2_height, + centered=(False, True, True), + ) + ) + back = ( + Cq.Workplane('XZ') + .cylinder( + radius=self.back_width / 2, + height=self.back_height, + centered=True, + ) + .box( + length=self.back_hole_ext, + width=self.back_width, + height=self.back_height, + combine=True, + centered=(False, True, True) + ) + .copyWorkplane(Cq.Workplane('XZ')) + .cylinder( + radius=self.back_hole_diam / 2, + height=self.back_height, + centered=True, + combine='cut', + ) + ) + result = ( + Cq.Assembly() + .add(front, name="front", + loc=Cq.Location((-self.front_hole_ext, 0, 0))) + .add(segment1, name="segment1", + loc=Cq.Location((stroke_x, 0, 0))) + .add(segment2, name="segment2", + loc=Cq.Location((stroke_x + self.segment1_length, 0, 0))) + .add(back, name="back", + loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180)) + ) + if shaft: + result.add(shaft, name="shaft") + return result + + +LINEAR_ACTUATOR_SHOULDER = LinearActuator( + mass=34.0, + stroke_length=30, +) +LINEAR_ACTUATOR_HEX_NUT = HexNut( + mass=0.8, + diam_thread=4, + pitch=0.7, + thickness=4.16, + width=6.79, +) +LINEAR_ACTUATOR_BOLT = FlatHeadBolt( + mass=1.7, + diam_head=16.68, + height_head=2.98, + diam_thread=4.0, + height_thread=15.83, +) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e5aa154..04df3f9 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -12,30 +12,64 @@ import nhf.utils TOL = 1e-6 +# Parts used + +# uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP +HS_JOINT_KNOB = 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, +) +# Tom's world 8Pcs M12-1.75 Hex Nut Assortment Set Stainless Steel 304(18-8) +# Metric Hexagon Nut for Bolts, Bright Finish, Full Thread (M12) +HS_JOINT_HEX_NUT = HexNut( + mass=14.9, + diam_thread=12.0, + pitch=1.75, + thickness=9.7, + width=18.9, +) +SHOULDER_AXIS_BOLT = FlatHeadBolt( + # FIXME: measure + diam_head=10.0, + height_head=3.0, + diam_thread=6.0, + height_thread=20.0, + mass=float('nan'), +) +# Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional +# Springs Repair Maintenance Spring +SHOULDER_TORSION_SPRING = TorsionSpring( + mass=2.2, + # inner diameter = 9 + radius=9/2 + 1.2, + thickness=1.3, + height=7.5, +) +# KALIONE 10 Pieces Torsion Spring, Stainless Steel Small Torsion Springs, Tiny +# Torsional Spring, 90° Deflection Compression Spring Kit for Repair Tools +# Woodworking DIY, 50mm +ELBOW_TORSION_SPRING = TorsionSpring( + mass=1.7, + radius=9 / 2, + thickness=1.3, + height=6.5, + tail_length=45.0, + right_handed=False, +) + @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, - ) + knob: ThreaddedKnob = HS_JOINT_KNOB + hex_nut: HexNut = HS_JOINT_HEX_NUT hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, radius_inner=15.0, @@ -206,14 +240,7 @@ class RootJoint(Model): @dataclass class ShoulderJoint(Model): - bolt: FlatHeadBolt = FlatHeadBolt( - # FIXME: measure - diam_head=10.0, - height_head=3.0, - diam_thread=6.0, - height_thread=20.0, - mass=float('nan'), - ) + bolt: FlatHeadBolt = SHOULDER_AXIS_BOLT height: float = 70.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( @@ -225,13 +252,7 @@ class ShoulderJoint(Model): track_disk_height=5.0, rider_disk_height=5.0, radius_axle=3.0, - spring=TorsionSpring( - mass=float('nan'), - # inner diameter = 9 - radius=9/2 + 1.2, - thickness=1.3, - height=7.5, - ), + spring=SHOULDER_TORSION_SPRING, rider_slot_begin=0, rider_n_slots=1, rider_slot_span=0, @@ -451,12 +472,12 @@ class ShoulderJoint(Model): .addS(self.child(), name="child", role=Role.CHILD, material=mat) .constrain("child/core", "Fixed") - .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", + .addS(self.torsion_joint.spring.assembly(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) - .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", + .addS(self.torsion_joint.spring.assembly(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) .addS(self.parent_bot(), name="parent_bot", @@ -558,14 +579,7 @@ class DiskJoint(Model): We embed a spring inside the joint, with one leg in the disk and one leg in the housing. This provides torsion resistance. """ - spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( - mass=float('nan'), - radius=9 / 2, - thickness=1.3, - height=6.5, - tail_length=45.0, - right_handed=False, - )) + spring: TorsionSpring = ELBOW_TORSION_SPRING radius_housing: float = 22.0 radius_disk: float = 20.0 @@ -787,7 +801,7 @@ class DiskJoint(Model): ( assembly .addS( - self.spring.generate(deflection=-deflection), + self.spring.assembly(deflection=-deflection), name=spring_name, role=Role.DAMPING, material=Material.STEEL_SPRING)