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 from typing import Union, Optional
import cadquery as Cq import cadquery as Cq
import nhf.parts.metric_threads as metric_threads import nhf.parts.metric_threads as metric_threads
import nhf.utils
class Mount: class Mount:
""" """
Describes the internal connection between two cylinders 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: def external_thread(self, length: float) -> Cq.Shape:
""" """
@ -29,7 +30,7 @@ class Mount:
""" """
@dataclass @dataclass
class ThreadedJoint(Mount): class ThreadedMount(Mount):
pitch: float = 3 pitch: float = 3
@ -37,13 +38,13 @@ class ThreadedJoint(Mount):
# standard. This determines the wall thickness of the insertion. # standard. This determines the wall thickness of the insertion.
diam_threading: float = 27 diam_threading: float = 27
def diam_insertion_internal(self): def diam_insertion_internal(self) -> float:
r = metric_threads.metric_thread_major_radius( r = metric_threads.metric_thread_major_radius(
self.diam_threading, self.diam_threading,
self.pitch, self.pitch,
internal=True) internal=True)
return r * 2 return r * 2
def diam_connector_external(self): def diam_connector_external(self) -> float:
r = metric_threads.metric_thread_minor_radius( r = metric_threads.metric_thread_minor_radius(
self.diam_threading, self.diam_threading,
self.pitch) self.pitch)
@ -64,9 +65,94 @@ class ThreadedJoint(Mount):
@dataclass @dataclass
class BayonetMount(Mount): 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 @dataclass
class Handle: class Handle:
@ -75,7 +161,7 @@ class Handle:
This assumes the handle segment material does not have threads. Each segment This assumes the handle segment material does not have threads. Each segment
attaches to two insertions, which have threads on the inside. A connector 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). Note that all the radial sizes are diameters (in mm).
""" """
@ -84,7 +170,7 @@ class Handle:
diam: float = 38 diam: float = 38
diam_inner: float = 33 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 # Internal cavity diameter. This determines the wall thickness of the connector
diam_connector_internal: float = 18.0 diam_connector_internal: float = 18.0
@ -102,10 +188,10 @@ class Handle:
def __post_init__(self): def __post_init__(self):
assert self.diam > self.diam_inner, "Material thickness cannot be <= 0" assert self.diam > self.diam_inner, "Material thickness cannot be <= 0"
if self.joint: if self.mount:
assert self.diam_inner > self.joint.diam_insertion_internal(), "Threading radius is too big" assert self.diam_inner > self.mount.diam_insertion_internal(), "Threading radius is too big"
assert self.joint.diam_insertion_internal() > self.joint.diam_connector_external() assert self.mount.diam_insertion_internal() >= self.mount.diam_connector_external()
assert self.joint.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" assert self.mount.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large"
assert self.insertion_length > self.rim_length assert self.insertion_length > self.rim_length
def segment(self, length: float): def segment(self, length: float):
@ -123,8 +209,8 @@ class Handle:
def insertion(self, holes=[]): def insertion(self, holes=[]):
""" """
This type of joint is used to connect two handlebar pieces. Each handlebar This type of mount is used to connect two handlebar pieces. Each handlebar
piece is a tube which cannot be machined, so the joint connects to the piece is a tube which cannot be machined, so the mount connects to the
handle by glue. handle by glue.
Tags: Tags:
@ -152,11 +238,12 @@ class Handle:
.circle(self.diam / 2) .circle(self.diam / 2)
.extrude(self.rim_length) .extrude(self.rim_length)
.faces(">Z") .faces(">Z")
.hole(self.joint.diam_insertion_internal()) .hole(self.mount.diam_insertion_internal())
) )
result.faces(">Z").tag("mate") result.faces(">Z").tag("mate")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X")
if not self.simplify_geometry: 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) result = result.union(thread)
for h in holes: for h in holes:
cyl = Cq.Solid.makeCylinder( cyl = Cq.Solid.makeCylinder(
@ -188,23 +275,24 @@ class Handle:
result result
.faces(selector) .faces(selector)
.workplane() .workplane()
.circle(self.joint.diam_connector_external() / 2) .circle(self.mount.diam_connector_external() / 2)
.extrude(self.insertion_length) .extrude(self.insertion_length)
) )
if not solid: if not solid:
result = result.faces(">Z").hole(self.diam_connector_internal) result = result.faces(">Z").hole(self.diam_connector_internal)
if not self.simplify_geometry: 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 = (
result result
.union( .union(
thread thread
.located(Cq.Location((0, 0, self.connector_length / 2)))) .located(Cq.Location((0, 0, -self.connector_length))))
.union( .union(
thread thread
.rotate((0,0,0), (1,0,0), angleDegrees=180) .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 return result
def one_side_connector(self, height=None): def one_side_connector(self, height=None):
@ -224,18 +312,19 @@ class Handle:
result result
.faces(">Z") .faces(">Z")
.workplane() .workplane()
.circle(self.joint.diam_connector_external() / 2) .circle(self.mount.diam_connector_external() / 2)
.extrude(self.insertion_length) .extrude(self.insertion_length)
) )
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X")
if not self.simplify_geometry: 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 = (
result result
.union( .union(
thread thread
# Avoids collision in some mating cases # Avoids collision in some mating cases
.rotate((0,0,0), (1,0,0), angleDegrees=180) .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 return result
@ -246,7 +335,7 @@ class Handle:
result = ( result = (
Cq.Workplane('XY') Cq.Workplane('XY')
.cylinder( .cylinder(
radius=self.joint.diam_connector_external / 2, radius=self.mount.diam_connector_external / 2,
height=length, height=length,
centered=(True, True, False), centered=(True, True, False),
) )
@ -254,7 +343,7 @@ class Handle:
result.faces(">Z").tag("mate") result.faces(">Z").tag("mate")
result.faces("<Z").tag("base") result.faces("<Z").tag("base")
if not self.simplify_geometry: if not self.simplify_geometry:
thread = self.joint.external_thread(length=length).val() thread = self.mount.external_thread(length=length).val()
result = ( result = (
result result
.union(thread) .union(thread)
@ -271,6 +360,8 @@ class Handle:
.add(self.insertion(), name="i2", color=insertion_color) .add(self.insertion(), name="i2", color=insertion_color)
.constrain("c?mate1", "i1?mate", "Plane") .constrain("c?mate1", "i1?mate", "Plane")
.constrain("c?mate2", "i2?mate", "Plane") .constrain("c?mate2", "i2?mate", "Plane")
.constrain("c?dir", "i1?dir", "Axis")
.constrain("c?dir", "i2?dir", "Axis")
.solve() .solve()
) )
return result return result
@ -282,6 +373,7 @@ class Handle:
.add(self.insertion(), name="i", color=connector_color) .add(self.insertion(), name="i", color=connector_color)
.add(self.one_side_connector(), name="c", color=insertion_color) .add(self.one_side_connector(), name="c", color=insertion_color)
.constrain("i?mate", "c?mate", "Plane") .constrain("i?mate", "c?mate", "Plane")
.constrain("c?dir", "i?dir", "Axis")
.solve() .solve()
) )
return result return result

View File

@ -58,25 +58,41 @@ class TestJoints(unittest.TestCase):
class TestHandle(unittest.TestCase): class TestHandle(unittest.TestCase):
def test_handle_collision(self): def test_threaded_collision(self):
h = handle.Handle() h = handle.Handle(mount=handle.ThreadedMount())
assembly = h.connector_insertion_assembly() assembly = h.connector_insertion_assembly()
self.assertEqual(pairwise_intersection(assembly), []) self.assertEqual(pairwise_intersection(assembly), [])
def test_handle_assembly(self): def test_threaded_assembly(self):
h = handle.Handle() h = handle.Handle(mount=handle.ThreadedMount())
assembly = h.connector_insertion_assembly() assembly = h.connector_insertion_assembly()
bbox = assembly.toCompound().BoundingBox() bbox = assembly.toCompound().BoundingBox()
self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.xlen, h.diam)
self.assertAlmostEqual(bbox.ylen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam)
def test_threaded_one_sided_insertion(self):
def test_one_sided_insertion(self): h = handle.Handle(mount=handle.ThreadedMount())
h = handle.Handle()
assembly = h.connector_one_side_insertion_assembly() assembly = h.connector_one_side_insertion_assembly()
bbox = assembly.toCompound().BoundingBox() bbox = assembly.toCompound().BoundingBox()
self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.xlen, h.diam)
self.assertAlmostEqual(bbox.ylen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam)
self.assertEqual(pairwise_intersection(assembly), []) 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): class TestMetricThreads(unittest.TestCase):

View File

@ -34,7 +34,7 @@ import cadquery as Cq
from nhf import Material, Role from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly from nhf.build import Model, TargetKind, target, assembly
from nhf.parts.joints import HirthJoint, TorsionJoint 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.wing as MW
import nhf.touhou.houjuu_nue.trident as MT import nhf.touhou.houjuu_nue.trident as MT
import nhf.utils import nhf.utils
@ -129,7 +129,9 @@ class Parameters(Model):
diam_inner=38-2 * 25.4/8, diam_inner=38-2 * 25.4/8,
diam_connector_internal=18, diam_connector_internal=18,
simplify_geometry=False, simplify_geometry=False,
mount=BayonetMount(n_pin=3),
)) ))
trident_terminal_height: float = 60
material_panel: Material = Material.ACRYLIC_TRANSPARENT material_panel: Material = Material.ACRYLIC_TRANSPARENT
material_bracket: Material = Material.ACRYLIC_TRANSPARENT material_bracket: Material = Material.ACRYLIC_TRANSPARENT
@ -147,6 +149,12 @@ class Parameters(Model):
@target(name="trident/handle-insertion") @target(name="trident/handle-insertion")
def handle_insertion(self): def handle_insertion(self):
return self.trident_handle.insertion() 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: def harness_profile(self) -> Cq.Sketch: