cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
3 changed files with 152 additions and 36 deletions
Showing only changes of commit 539a5d1229 - Show all commits

View File

@ -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

View File

@ -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):

View File

@ -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: