cosplay: Touhou/Houjuu Nue #4
|
@ -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("<Z").tag("base")
|
||||
if not self.simplify_geometry:
|
||||
thread = self.joint.external_thread(length=length).val()
|
||||
thread = self.mount.external_thread(length=length).val()
|
||||
result = (
|
||||
result
|
||||
.union(thread)
|
||||
|
@ -271,6 +360,8 @@ class Handle:
|
|||
.add(self.insertion(), name="i2", color=insertion_color)
|
||||
.constrain("c?mate1", "i1?mate", "Plane")
|
||||
.constrain("c?mate2", "i2?mate", "Plane")
|
||||
.constrain("c?dir", "i1?dir", "Axis")
|
||||
.constrain("c?dir", "i2?dir", "Axis")
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
@ -282,6 +373,7 @@ class Handle:
|
|||
.add(self.insertion(), name="i", color=connector_color)
|
||||
.add(self.one_side_connector(), name="c", color=insertion_color)
|
||||
.constrain("i?mate", "c?mate", "Plane")
|
||||
.constrain("c?dir", "i?dir", "Axis")
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -58,25 +58,41 @@ class TestJoints(unittest.TestCase):
|
|||
|
||||
class TestHandle(unittest.TestCase):
|
||||
|
||||
def test_handle_collision(self):
|
||||
h = handle.Handle()
|
||||
def test_threaded_collision(self):
|
||||
h = handle.Handle(mount=handle.ThreadedMount())
|
||||
assembly = h.connector_insertion_assembly()
|
||||
self.assertEqual(pairwise_intersection(assembly), [])
|
||||
def test_handle_assembly(self):
|
||||
h = handle.Handle()
|
||||
def test_threaded_assembly(self):
|
||||
h = handle.Handle(mount=handle.ThreadedMount())
|
||||
assembly = h.connector_insertion_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.xlen, h.diam)
|
||||
self.assertAlmostEqual(bbox.ylen, h.diam)
|
||||
|
||||
def test_one_sided_insertion(self):
|
||||
h = handle.Handle()
|
||||
def test_threaded_one_sided_insertion(self):
|
||||
h = handle.Handle(mount=handle.ThreadedMount())
|
||||
assembly = h.connector_one_side_insertion_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.xlen, h.diam)
|
||||
self.assertAlmostEqual(bbox.ylen, h.diam)
|
||||
self.assertEqual(pairwise_intersection(assembly), [])
|
||||
def test_bayonet_collision(self):
|
||||
h = handle.Handle(mount=handle.BayonetMount())
|
||||
assembly = h.connector_insertion_assembly()
|
||||
self.assertEqual(pairwise_intersection(assembly), [])
|
||||
def test_bayonet_assembly(self):
|
||||
h = handle.Handle(mount=handle.BayonetMount())
|
||||
assembly = h.connector_insertion_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.xlen, h.diam)
|
||||
self.assertAlmostEqual(bbox.ylen, h.diam)
|
||||
|
||||
def test_bayonet_one_sided_insertion(self):
|
||||
h = handle.Handle(mount=handle.BayonetMount())
|
||||
assembly = h.connector_one_side_insertion_assembly()
|
||||
bbox = assembly.toCompound().BoundingBox()
|
||||
self.assertAlmostEqual(bbox.xlen, h.diam)
|
||||
self.assertAlmostEqual(bbox.ylen, h.diam)
|
||||
self.assertEqual(pairwise_intersection(assembly), [])
|
||||
|
||||
class TestMetricThreads(unittest.TestCase):
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import cadquery as Cq
|
|||
from nhf import Material, Role
|
||||
from nhf.build import Model, TargetKind, target, assembly
|
||||
from nhf.parts.joints import HirthJoint, TorsionJoint
|
||||
from nhf.parts.handle import Handle
|
||||
from nhf.parts.handle import Handle, BayonetMount
|
||||
import nhf.touhou.houjuu_nue.wing as MW
|
||||
import nhf.touhou.houjuu_nue.trident as MT
|
||||
import nhf.utils
|
||||
|
@ -129,7 +129,9 @@ class Parameters(Model):
|
|||
diam_inner=38-2 * 25.4/8,
|
||||
diam_connector_internal=18,
|
||||
simplify_geometry=False,
|
||||
mount=BayonetMount(n_pin=3),
|
||||
))
|
||||
trident_terminal_height: float = 60
|
||||
|
||||
material_panel: Material = Material.ACRYLIC_TRANSPARENT
|
||||
material_bracket: Material = Material.ACRYLIC_TRANSPARENT
|
||||
|
@ -147,6 +149,12 @@ class Parameters(Model):
|
|||
@target(name="trident/handle-insertion")
|
||||
def handle_insertion(self):
|
||||
return self.trident_handle.insertion()
|
||||
@target(name="trident/proto-handle-terminal-connector")
|
||||
def handle_experimental_connector(self):
|
||||
return self.trident_handle.one_side_connector(height=15)
|
||||
@target(name="trident/handle-terminal-connector")
|
||||
def handle_terminal_connector(self):
|
||||
return self.trident_handle.one_side_connector(height=self.trident_terminal_height)
|
||||
|
||||
|
||||
def harness_profile(self) -> Cq.Sketch:
|
||||
|
|
Loading…
Reference in New Issue