Cosplay/nhf/parts/handle.py

386 lines
12 KiB
Python
Raw Normal View History

2024-06-25 06:11:48 -07:00
"""
This schematics file contains all designs related to tool handles
"""
from dataclasses import dataclass, field
from typing import Union, Optional
2024-06-25 06:11:48 -07:00
import cadquery as Cq
2024-07-04 00:42:14 -07:00
import nhf.parts.metric_threads as metric_threads
2024-07-09 22:09:16 -07:00
import nhf.utils
2024-06-25 06:11:48 -07:00
class Mount:
"""
Describes the internal connection between two cylinders
"""
2024-07-09 22:09:16 -07:00
def diam_insertion_internal(self) -> float:
"""
2024-07-09 22:09:16 -07:00
Diameter of the internal cavity in the insertion
"""
2024-07-09 22:09:16 -07:00
def diam_connector_external(self) -> float:
"""
2024-07-09 22:09:16 -07:00
Diameter of the external size of the connector
"""
def external_thread(self, length: float) -> Cq.Shape:
"""
Generates the external connector
"""
def internal_thread(self, length: float) -> Cq.Shape:
"""
Generates the internal connector
"""
@dataclass
2024-07-09 22:09:16 -07:00
class ThreadedMount(Mount):
pitch: float = 3
# Major diameter of the internal threads, following ISO metric screw thread
# standard. This determines the wall thickness of the insertion.
diam_threading: float = 27
2024-07-09 22:09:16 -07:00
def diam_insertion_internal(self) -> float:
r = metric_threads.metric_thread_major_radius(
self.diam_threading,
self.pitch,
internal=True)
return r * 2
2024-07-09 22:09:16 -07:00
def diam_connector_external(self) -> float:
r = metric_threads.metric_thread_minor_radius(
self.diam_threading,
self.pitch)
return r * 2
def external_thread(self, length: float):
return metric_threads.external_metric_thread(
self.diam_threading,
self.pitch,
length,
top_lead_in=True)
def internal_thread(self, length: float):
return metric_threads.internal_metric_thread(
self.diam_threading,
self.pitch,
length)
@dataclass
class BayonetMount(Mount):
"""
2024-07-09 22:09:16 -07:00
Bayonet type connection
"""
2024-07-09 22:09:16 -07:00
diam_outer: float = 30
diam_inner: float = 27
# Angular span (in degrees) of the slider
pin_span: float = 15
pin_height: float = 5
2024-07-09 22:30:29 -07:00
# Wall at the bottom of the slot
gap: float = 3
2024-07-09 22:09:16 -07:00
# Angular span (in degrees) of the slot
slot_span: float = 90
# Number of pins equally distributed along a circle
n_pin: int = 2
2024-07-09 22:30:29 -07:00
2024-07-09 22:09:16 -07:00
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):
2024-07-09 22:30:29 -07:00
assert length > self.pin_height + self.gap
2024-07-09 22:09:16 -07:00
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')
2024-07-09 22:30:29 -07:00
.workplane(offset=self.gap)
2024-07-09 22:09:16 -07:00
.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):
2024-07-09 22:30:29 -07:00
assert length > self.pin_height + self.gap
2024-07-09 22:09:16 -07:00
slot = (
Cq.Workplane('XY')
.cylinder(
2024-07-09 22:30:29 -07:00
height=length - self.gap,
2024-07-09 22:09:16 -07:00
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),
)
2024-07-09 22:30:29 -07:00
.copyWorkplane(Cq.Workplane('XY'))
.workplane(offset=self.gap)
.polarArray(radius=0, startAngle=self.slot_span, angle=360, count=self.n_pin)
2024-07-09 22:09:16 -07:00
.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
2024-07-03 18:45:16 -07:00
@dataclass
2024-06-25 06:11:48 -07:00
class Handle:
"""
Characteristic of a tool handle
2024-06-26 08:28:25 -07:00
This assumes the handle segment material does not have threads. Each segment
attaches to two insertions, which have threads on the inside. A connector
2024-07-09 22:09:16 -07:00
has threads on the outside and mounts two insertions.
2024-06-26 08:28:25 -07:00
Note that all the radial sizes are diameters (in mm).
2024-06-25 06:11:48 -07:00
"""
# Outer and inner radii for the handle usually come in standard sizes
2024-06-26 08:28:25 -07:00
diam: float = 38
diam_inner: float = 33
2024-07-09 22:09:16 -07:00
mount: Optional[Mount] = field(default_factory=lambda: ThreadedMount())
2024-06-25 06:11:48 -07:00
2024-06-26 08:28:25 -07:00
# Internal cavity diameter. This determines the wall thickness of the connector
diam_connector_internal: float = 18.0
2024-06-25 06:11:48 -07:00
# If set to true, do not generate the connections
2024-06-26 08:28:25 -07:00
simplify_geometry: bool = True
2024-06-25 06:11:48 -07:00
# Length for the rim on the female connector
rim_length: float = 5
2024-07-02 19:59:09 -07:00
insertion_length: float = 30
2024-06-25 06:11:48 -07:00
2024-06-26 08:28:25 -07:00
# Amount by which the connector goes into the segment
2024-06-25 06:11:48 -07:00
connector_length: float = 60
def __post_init__(self):
2024-06-26 08:28:25 -07:00
assert self.diam > self.diam_inner, "Material thickness cannot be <= 0"
2024-07-09 22:09:16 -07:00
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"
2024-06-25 06:11:48 -07:00
assert self.insertion_length > self.rim_length
def segment(self, length: float):
result = (
Cq.Workplane()
2024-06-26 08:28:25 -07:00
.cylinder(
radius=self.diam / 2,
height=length)
2024-07-07 21:45:10 -07:00
.faces(">Z")
.hole(self.diam_inner)
2024-06-25 06:11:48 -07:00
)
result.faces("<Z").tag("mate1")
result.faces(">Z").tag("mate2")
return result
2024-07-01 17:59:42 -07:00
def insertion(self, holes=[]):
2024-06-25 06:11:48 -07:00
"""
2024-07-09 22:09:16 -07:00
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
2024-06-25 06:11:48 -07:00
handle by glue.
Tags:
* lip: Co-planar Mates to the rod
* mate: Mates to the connector
2024-07-01 17:59:42 -07:00
WARNING: A tolerance lower than the defualt (maybe 5e-4) is required for
STL export.
Set `holes` to the heights for drilling holes into the model for resin
to flow out.
2024-06-25 06:11:48 -07:00
"""
result = (
Cq.Workplane('XY')
.cylinder(
2024-06-26 08:28:25 -07:00
radius=self.diam_inner / 2,
2024-06-25 06:11:48 -07:00
height=self.insertion_length - self.rim_length,
centered=[True, True, False])
)
2024-06-26 08:28:25 -07:00
result.faces(">Z").tag("rim")
if self.rim_length > 0:
result = (
result.faces(">Z")
.workplane()
.circle(self.diam / 2)
.extrude(self.rim_length)
.faces(">Z")
2024-07-09 22:09:16 -07:00
.hole(self.mount.diam_insertion_internal())
2024-06-26 08:28:25 -07:00
)
2024-06-25 06:11:48 -07:00
result.faces(">Z").tag("mate")
2024-07-09 22:09:16 -07:00
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X")
2024-06-26 08:28:25 -07:00
if not self.simplify_geometry:
2024-07-09 22:09:16 -07:00
thread = self.mount.internal_thread(self.insertion_length).val()
2024-06-26 08:28:25 -07:00
result = result.union(thread)
2024-07-01 17:59:42 -07:00
for h in holes:
cyl = Cq.Solid.makeCylinder(
radius=2,
height=self.diam * 2,
pnt=(-self.diam, 0, h),
dir=(1, 0, 0))
result = result.cut(cyl)
2024-06-25 06:11:48 -07:00
return result
2024-07-01 17:59:42 -07:00
def connector(self, solid: bool = True):
2024-06-25 06:11:48 -07:00
"""
Tags:
* mate{1,2}: Mates to the connector
2024-07-01 17:59:42 -07:00
WARNING: A tolerance lower than the defualt (maybe 2e-4) is required for
STL export.
2024-06-25 06:11:48 -07:00
"""
result = (
Cq.Workplane('XY')
.cylinder(
2024-06-26 08:28:25 -07:00
radius=self.diam / 2,
2024-06-25 06:11:48 -07:00
height=self.connector_length,
)
)
for (tag, selector) in [("mate1", "<Z"), ("mate2", ">Z")]:
result.faces(selector).tag(tag)
result = (
result
.faces(selector)
.workplane()
2024-07-09 22:09:16 -07:00
.circle(self.mount.diam_connector_external() / 2)
2024-06-25 06:11:48 -07:00
.extrude(self.insertion_length)
)
if not solid:
2024-06-26 08:28:25 -07:00
result = result.faces(">Z").hole(self.diam_connector_internal)
if not self.simplify_geometry:
2024-07-09 22:09:16 -07:00
thread = self.mount.external_thread(self.insertion_length).val()
2024-06-26 08:28:25 -07:00
result = (
result
.union(
thread
2024-07-09 22:09:16 -07:00
.located(Cq.Location((0, 0, -self.connector_length))))
2024-06-26 08:28:25 -07:00
.union(
thread
2024-07-01 17:59:42 -07:00
.rotate((0,0,0), (1,0,0), angleDegrees=180)
2024-07-09 22:09:16 -07:00
.located(Cq.Location((0, 0, self.connector_length))))
2024-06-26 08:28:25 -07:00
)
2024-07-09 22:09:16 -07:00
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X")
2024-06-25 06:11:48 -07:00
return result
2024-07-02 19:59:09 -07:00
def one_side_connector(self, height=None):
if height is None:
height = self.rim_length
2024-06-25 06:11:48 -07:00
result = (
Cq.Workplane('XY')
.cylinder(
2024-06-26 08:28:25 -07:00
radius=self.diam / 2,
2024-07-02 19:59:09 -07:00
height=height,
centered=(True, True, False)
2024-06-25 06:11:48 -07:00
)
)
2024-06-26 09:01:01 -07:00
result.faces(">Z").tag("mate")
result.faces("<Z").tag("base")
2024-06-25 06:11:48 -07:00
result = (
result
2024-06-26 09:01:01 -07:00
.faces(">Z")
2024-06-25 06:11:48 -07:00
.workplane()
2024-07-09 22:09:16 -07:00
.circle(self.mount.diam_connector_external() / 2)
2024-06-25 06:11:48 -07:00
.extrude(self.insertion_length)
)
2024-07-09 22:09:16 -07:00
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X")
2024-06-26 09:01:01 -07:00
if not self.simplify_geometry:
2024-07-09 22:09:16 -07:00
thread = self.mount.external_thread(self.insertion_length).val()
2024-06-26 09:01:01 -07:00
result = (
result
.union(
thread
2024-07-07 21:45:10 -07:00
# Avoids collision in some mating cases
.rotate((0,0,0), (1,0,0), angleDegrees=180)
2024-07-09 22:09:16 -07:00
.located(Cq.Location((0, 0, height + self.insertion_length))))
2024-06-26 09:01:01 -07:00
)
2024-06-25 06:11:48 -07:00
return result
2024-07-01 17:59:42 -07:00
def threaded_core(self, length):
"""
Generates a threaded core for unioning with other components
"""
result = (
Cq.Workplane('XY')
.cylinder(
2024-07-09 22:09:16 -07:00
radius=self.mount.diam_connector_external / 2,
2024-07-01 17:59:42 -07:00
height=length,
centered=(True, True, False),
)
)
result.faces(">Z").tag("mate")
result.faces("<Z").tag("base")
if not self.simplify_geometry:
2024-07-09 22:09:16 -07:00
thread = self.mount.external_thread(length=length).val()
2024-07-01 17:59:42 -07:00
result = (
result
.union(thread)
)
return result
2024-06-25 06:11:48 -07:00
def connector_insertion_assembly(self):
connector_color = Cq.Color(0.8,0.8,0.5,0.3)
insertion_color = Cq.Color(0.7,0.7,0.7,0.3)
result = (
Cq.Assembly()
.add(self.connector(), name="c", color=connector_color)
.add(self.insertion(), name="i1", color=insertion_color)
.add(self.insertion(), name="i2", color=insertion_color)
.constrain("c?mate1", "i1?mate", "Plane")
.constrain("c?mate2", "i2?mate", "Plane")
2024-07-09 22:09:16 -07:00
.constrain("c?dir", "i1?dir", "Axis")
.constrain("c?dir", "i2?dir", "Axis")
2024-06-25 06:11:48 -07:00
.solve()
)
return result
def connector_one_side_insertion_assembly(self):
connector_color = Cq.Color(0.8,0.8,0.5,0.3)
insertion_color = Cq.Color(0.7,0.7,0.7,0.3)
result = (
Cq.Assembly()
.add(self.insertion(), name="i", color=connector_color)
.add(self.one_side_connector(), name="c", color=insertion_color)
.constrain("i?mate", "c?mate", "Plane")
2024-07-09 22:09:16 -07:00
.constrain("c?dir", "i?dir", "Axis")
2024-06-25 06:11:48 -07:00
.solve()
)
return result