diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index c6a0afe..78d7cfc 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -4,6 +4,42 @@ import cadquery as Cq from nhf import Item, Role import nhf.utils +@dataclass(frozen=True) +class FlatHeadBolt(Item): + diam_head: float + height_head: float + diam_thread: float + height_thread: float + + @property + def name(self) -> str: + return f"Bolt M{int(self.diam_thread)} h{int(self.height_thread)}mm" + + + def generate(self) -> Cq.Assembly: + print(self.name) + head = Cq.Solid.makeCylinder( + radius=self.diam_head / 2, + height=self.height_head, + ) + rod = ( + Cq.Workplane('XY') + .cylinder( + radius=self.diam_thread/ 2, + height=self.height_thread, + centered=(True, True, False)) + ) + rod.faces("Z").tag("root") + + return ( + Cq.Assembly() + .addS(rod, name="thread", role=Role.CONNECTION) + .addS(head, name="head", role=Role.CONNECTION, + loc=Cq.Location((0, 0, self.height_thread))) + ) + + @dataclass(frozen=True) class ThreaddedKnob(Item): """ @@ -12,8 +48,8 @@ class ThreaddedKnob(Item): > Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening > Screws """ - diam_rod: float - height_rod: float + diam_thread: float + height_thread: float diam_knob: float diam_neck: float @@ -22,7 +58,7 @@ class ThreaddedKnob(Item): @property def name(self) -> str: - return f"Knob-M{int(self.diam_rod)}-{int(self.height_rod)}mm" + return f"Knob M{int(self.diam_thread)} h{int(self.height_thread)}mm" def generate(self) -> Cq.Assembly: print(self.name) @@ -34,41 +70,40 @@ class ThreaddedKnob(Item): radius=self.diam_neck / 2, height=self.height_neck, ) - rod = ( + thread = ( Cq.Workplane('XY') .cylinder( - radius=self.diam_rod / 2, - height=self.height_rod, + radius=self.diam_thread / 2, + height=self.height_thread, centered=(True, True, False)) ) - rod.faces("Z").tag("root") + thread.faces("Z").tag("root") return ( Cq.Assembly() - .addS(rod, name="rod", role=Role.CONNECTION) + .addS(thread, name="thread", role=Role.CONNECTION) .addS(neck, name="neck", role=Role.HANDLE, - loc=Cq.Location((0, 0, self.height_rod))) + loc=Cq.Location((0, 0, self.height_thread))) .addS(knob, name="knob", role=Role.HANDLE, - loc=Cq.Location((0, 0, self.height_rod + self.height_neck))) + loc=Cq.Location((0, 0, self.height_thread + self.height_neck))) ) @dataclass(frozen=True) class HexNut(Item): - diam: float + diam_thread: float pitch: float - # FIXME: Measure these - m: float - s: float + thickness: float + width: float def __post_init__(self): - assert self.s > self.diam + assert self.width > self.diam_thread @property def name(self): - return f"HexNut-M{int(self.diam)}-{self.pitch}" + return f"HexNut M{int(self.diam_thread)}-{self.pitch}" @property def role(self): @@ -76,14 +111,14 @@ class HexNut(Item): def generate(self) -> Cq.Workplane: print(self.name) - r = self.s / math.sqrt(3) + r = self.width / math.sqrt(3) result = ( Cq.Workplane("XY") .sketch() .regularPolygon(r=r, n=6) - .circle(r=self.diam/2, mode='s') + .circle(r=self.diam_thread/2, mode='s') .finalize() - .extrude(self.m) + .extrude(self.thickness) ) result.faces("Z").tag("top") diff --git a/nhf/parts/item.py b/nhf/parts/item.py index 16fbef7..1e321fc 100644 --- a/nhf/parts/item.py +++ b/nhf/parts/item.py @@ -31,17 +31,17 @@ class Item: def role(self) -> Optional[Role]: return None - def generate(self) -> Union[Cq.Assembly, Cq.Workplane]: + def generate(self, **kwargs) -> Union[Cq.Assembly, Cq.Workplane]: """ Creates an assembly for this item. Subclass should implement this """ return Cq.Assembly() - def assembly(self) -> Cq.Assembly: + def assembly(self, **kwargs) -> Cq.Assembly: """ Interface for creating assembly with the necessary metadata """ - a = self.generate() + a = self.generate(**kwargs) if isinstance(a, Cq.Workplane): a = Cq.Assembly(a) if role := self.role: diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 8602147..31bb686 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -180,6 +180,7 @@ class TorsionJoint: 3. An outer and an inner annuli which forms a track the rider can move on """ spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( + mass=float('nan'), radius=10.0, thickness=2.0, height=15.0, @@ -306,6 +307,7 @@ class TorsionJoint: .hole(self.radius_axle * 2) .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) ) + result.faces(" str: + return f"TorsionSpring-{int(self.radius)}-{int(self.height)}" + @property def radius_inner(self) -> float: return self.radius - self.thickness @@ -28,7 +33,7 @@ class TorsionSpring: def torque_at(self, theta: float) -> float: return self.torsion_rate * theta - def generate(self, deflection: float = 0): + def generate(self, deflection: float = 0) -> Cq.Workplane: omega = self.angle_neutral + deflection omega = -omega if self.right_handed else omega base = ( @@ -39,7 +44,6 @@ class TorsionSpring: base.faces(">Z").tag("top") base.faces(" Cq.Location: + """ + 2d location of the arm surface on the parent side, relative to axle + """ + return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.parent_lip_ext, 0, 0) + def parent(self, top: bool = False) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root @@ -219,14 +239,18 @@ 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) ) TorsionJoint.add_constraints( result, @@ -318,6 +342,7 @@ class DiskJoint(Model): 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, @@ -617,7 +642,7 @@ class ElbowJoint(Model): parent_arm_angle: float = 180.0 # Size of the mounting holes - hole_diam: float = 6.0 + hole_diam: float = 4.0 material: Material = Material.RESIN_TRANSPERENT @@ -628,6 +653,10 @@ class ElbowJoint(Model): assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 + @property + def total_thickness(self): + return self.disk_joint.total_thickness + def parent_arm_loc(self) -> Cq.Location: """ 2d Location of the centre of the arm surface on the parent side, assuming diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b9e2014..77f51f8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -41,8 +41,8 @@ class WingProfile(Model): )) shoulder_angle_bias: float = 0.0 shoulder_width: float = 36.0 - shoulder_tip_x: float = -200.0 - shoulder_tip_y: float = 160.0 + shoulder_tip_x: float = -260.0 + shoulder_tip_y: float = 165.0 shoulder_mid_x: float = -105.0 shoulder_mid_y: float = 75.0 @@ -102,7 +102,11 @@ class WingProfile(Model): self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) + assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) + 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) @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: @@ -225,6 +229,9 @@ class WingProfile(Model): (-self.base_width, 0), ) .assemble() + .push([self.shoulder_axle_loc.to2d_pos()]) + .circle(self.shoulder_joint.radius, mode='a') + .circle(self.shoulder_joint.bolt.diam_head / 2, mode='s') ) return sketch @@ -292,17 +299,12 @@ class WingProfile(Model): 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 - sw = self.shoulder_width - - axle_dist = self.shoulder_joint.parent_lip_ext - theta = math.radians(self.shoulder_angle_neutral) - c, s = math.cos(theta), math.sin(theta) + loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) tags = [ - # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) - ("shoulder", Cq.Location.from2d( - self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, - self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, - -self.shoulder_angle_neutral)), + ("shoulder", + self.shoulder_axle_loc * + self.shoulder_joint.parent_arm_loc() * + loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ] result = extrude_with_markers(