diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index 19fe15a..dc6a01f 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -5,19 +5,20 @@ from dataclasses import dataclass, field from typing import Union, Optional import cadquery as Cq import nhf.parts.metric_threads as metric_threads +import nhf.utils class Mount: """ Describes the internal connection between two cylinders """ - def diam_insertion_internal(self): + def diam_insertion_internal(self) -> float: """ - Maximum permitted diameter of the internal cavity + Diameter of the internal cavity in the insertion """ - def diam_connector_external(self): + def diam_connector_external(self) -> float: """ - Maximum permitted diameter of the external size of the insertion + Diameter of the external size of the connector """ def external_thread(self, length: float) -> Cq.Shape: """ @@ -29,7 +30,7 @@ class Mount: """ @dataclass -class ThreadedJoint(Mount): +class ThreadedMount(Mount): pitch: float = 3 @@ -37,13 +38,13 @@ class ThreadedJoint(Mount): # standard. This determines the wall thickness of the insertion. diam_threading: float = 27 - def diam_insertion_internal(self): + def diam_insertion_internal(self) -> float: r = metric_threads.metric_thread_major_radius( self.diam_threading, self.pitch, internal=True) return r * 2 - def diam_connector_external(self): + def diam_connector_external(self) -> float: r = metric_threads.metric_thread_minor_radius( self.diam_threading, self.pitch) @@ -64,9 +65,94 @@ class ThreadedJoint(Mount): @dataclass class BayonetMount(Mount): """ - Bayonet type joint + Bayonet type connection """ - pass + diam_outer: float = 30 + diam_inner: float = 27 + + # Angular span (in degrees) of the slider + pin_span: float = 15 + pin_height: float = 5 + + # Angular span (in degrees) of the slot + slot_span: float = 90 + + # Number of pins equally distributed along a circle + n_pin: int = 2 + + def __post_init__(self): + assert self.diam_outer > self.diam_inner + assert self.n_pin * self.slot_span < 360 + assert self.slot_span > self.pin_span + + def diam_insertion_internal(self) -> float: + return self.diam_outer + def diam_connector_external(self) -> float: + return self.diam_inner + + def external_thread(self, length: float): + assert length > self.pin_height + pin = ( + Cq.Workplane('XY') + .cylinder( + height=self.pin_height, + radius=self.diam_outer / 2, + angle=self.pin_span, + centered=(True, True, False)) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.pin_height, + radius=self.diam_inner / 2, + centered=(True, True, False), + combine="cut") + .val() + ) + result = ( + Cq.Workplane('XY') + .polarArray(radius=0, startAngle=0, angle=360, count=self.n_pin) + .eachpoint(lambda loc: pin.located(loc), combine='a') + .clean() + ) + return result + def internal_thread(self, length: float): + assert length > self.pin_height + slot = ( + Cq.Workplane('XY') + .cylinder( + height=length, + radius=self.diam_outer / 2, + angle=self.pin_span, + centered=(True, True, False) + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.pin_height, + radius=self.diam_outer / 2, + angle=self.slot_span, + centered=(True, True, False) + ) + .val() + ) + result = ( + Cq.Workplane('XY') + .cylinder( + height=length, + radius=self.diam_outer / 2, + centered=(True, True, False), + ) + .polarArray(radius=0, startAngle=-self.slot_span+self.pin_span, angle=360, count=self.n_pin) + .cutEach(lambda loc: slot.located(loc)) + .clean() + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=length, + radius=self.diam_inner / 2, + centered=(True, True, False), + combine="cut" + ) + ) + return result + @dataclass class Handle: @@ -75,7 +161,7 @@ class Handle: This assumes the handle segment material does not have threads. Each segment attaches to two insertions, which have threads on the inside. A connector - has threads on the outside and joints two insertions. + has threads on the outside and mounts two insertions. Note that all the radial sizes are diameters (in mm). """ @@ -84,7 +170,7 @@ class Handle: diam: float = 38 diam_inner: float = 33 - joint: Optional[Mount] = field(default_factory=lambda: ThreadedJoint()) + mount: Optional[Mount] = field(default_factory=lambda: ThreadedMount()) # Internal cavity diameter. This determines the wall thickness of the connector diam_connector_internal: float = 18.0 @@ -102,10 +188,10 @@ class Handle: def __post_init__(self): assert self.diam > self.diam_inner, "Material thickness cannot be <= 0" - if self.joint: - assert self.diam_inner > self.joint.diam_insertion_internal(), "Threading radius is too big" - assert self.joint.diam_insertion_internal() > self.joint.diam_connector_external() - assert self.joint.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" + if self.mount: + assert self.diam_inner > self.mount.diam_insertion_internal(), "Threading radius is too big" + assert self.mount.diam_insertion_internal() >= self.mount.diam_connector_external() + assert self.mount.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" assert self.insertion_length > self.rim_length def segment(self, length: float): @@ -123,8 +209,8 @@ class Handle: def insertion(self, holes=[]): """ - This type of joint is used to connect two handlebar pieces. Each handlebar - piece is a tube which cannot be machined, so the joint connects to the + This type of mount is used to connect two handlebar pieces. Each handlebar + piece is a tube which cannot be machined, so the mount connects to the handle by glue. Tags: @@ -152,11 +238,12 @@ class Handle: .circle(self.diam / 2) .extrude(self.rim_length) .faces(">Z") - .hole(self.joint.diam_insertion_internal()) + .hole(self.mount.diam_insertion_internal()) ) result.faces(">Z").tag("mate") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") if not self.simplify_geometry: - thread = self.joint.internal_thread(self.insertion_length).val() + thread = self.mount.internal_thread(self.insertion_length).val() result = result.union(thread) for h in holes: cyl = Cq.Solid.makeCylinder( @@ -188,23 +275,24 @@ class Handle: result .faces(selector) .workplane() - .circle(self.joint.diam_connector_external() / 2) + .circle(self.mount.diam_connector_external() / 2) .extrude(self.insertion_length) ) if not solid: result = result.faces(">Z").hole(self.diam_connector_internal) if not self.simplify_geometry: - thread = self.joint.external_thread(self.insertion_length).val() + thread = self.mount.external_thread(self.insertion_length).val() result = ( result .union( thread - .located(Cq.Location((0, 0, self.connector_length / 2)))) + .located(Cq.Location((0, 0, -self.connector_length)))) .union( thread .rotate((0,0,0), (1,0,0), angleDegrees=180) - .located(Cq.Location((0, 0, -self.connector_length / 2)))) + .located(Cq.Location((0, 0, self.connector_length)))) ) + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") return result def one_side_connector(self, height=None): @@ -224,18 +312,19 @@ class Handle: result .faces(">Z") .workplane() - .circle(self.joint.diam_connector_external() / 2) + .circle(self.mount.diam_connector_external() / 2) .extrude(self.insertion_length) ) + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") if not self.simplify_geometry: - thread = self.joint.external_thread(self.insertion_length).val() + thread = self.mount.external_thread(self.insertion_length).val() result = ( result .union( thread # Avoids collision in some mating cases .rotate((0,0,0), (1,0,0), angleDegrees=180) - .located(Cq.Location((0, 0, height)))) + .located(Cq.Location((0, 0, height + self.insertion_length)))) ) return result @@ -246,7 +335,7 @@ class Handle: result = ( Cq.Workplane('XY') .cylinder( - radius=self.joint.diam_connector_external / 2, + radius=self.mount.diam_connector_external / 2, height=length, centered=(True, True, False), ) @@ -254,7 +343,7 @@ class Handle: result.faces(">Z").tag("mate") result.faces(" Cq.Sketch: