cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
4 changed files with 232 additions and 76 deletions
Showing only changes of commit 876571418c - Show all commits

View File

@ -100,7 +100,7 @@ class HirthJoint:
(
result
.polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True)
.tag("directrix")
.tag("dir")
)
return result
@ -132,7 +132,7 @@ class HirthJoint:
.add(obj2, name="obj2", color=Role.CHILD.color)
.constrain("obj1", "Fixed")
.constrain("obj1?mate", "obj2?mate", "Plane")
.constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle)
.constrain("obj1?dir", "obj2?dir", "Axis", param=angle)
.solve()
)
return result
@ -249,7 +249,8 @@ class TorsionJoint:
"""
# Radius limit for rotating components
radius: float = 40
radius_track: float = 40
radius_rider: float = 38
track_disk_height: float = 10
rider_disk_height: float = 8
@ -281,7 +282,8 @@ class TorsionJoint:
def __post_init__(self):
assert self.radius > self.groove_radius_outer
assert self.radius_track > self.groove_radius_outer
assert self.radius_rider > self.groove_radius_outer
assert self.groove_radius_outer > self.groove_radius_inner
assert self.groove_radius_inner > self.radius_spring
assert self.spring_height > self.groove_depth, "Groove is too deep"
@ -289,8 +291,18 @@ class TorsionJoint:
@property
def total_height(self):
"""
Total height counting from bottom to top
"""
return self.track_disk_height + self.rider_disk_height + self.spring_height
@property
def radius(self):
"""
Maximum radius of this joint
"""
return max(self.radius_rider, self.radius_track)
@property
def _radius_spring_internal(self):
return self.radius_spring - self.spring_thickness
@ -335,14 +347,14 @@ class TorsionJoint:
# TODO: Cover outer part of track only. Can we do this?
groove_profile = (
Cq.Sketch()
.circle(self.radius)
.circle(self.radius_track)
.circle(self.groove_radius_outer, mode='s')
.circle(self.groove_radius_inner, mode='a')
.circle(self.radius_spring, mode='s')
)
spring_hole_profile = (
Cq.Sketch()
.circle(self.radius)
.circle(self.radius_track)
.circle(self.radius_spring, mode='s')
)
slot_height = self.spring_thickness
@ -359,7 +371,7 @@ class TorsionJoint:
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.radius,
radius=self.radius_track,
height=self.track_disk_height,
centered=(True, True, False))
.faces('>Z')
@ -375,12 +387,12 @@ class TorsionJoint:
.hole(self.radius_axle * 2)
.cut(slot.moved(Cq.Location((0, 0, self.track_disk_height))))
)
# Insert directrix`
# Insert directrix
result.polyline(self._directrix(self.track_disk_height),
forConstruction=True).tag("directrix")
forConstruction=True).tag("dir")
return result
def rider(self, rider_slot_begin=None):
def rider(self, rider_slot_begin=None, reverse_directrix_label=False):
if not rider_slot_begin:
rider_slot_begin = self.rider_slot_begin
def slot(loc):
@ -389,11 +401,11 @@ class TorsionJoint:
return face.located(loc)
wall_profile = (
Cq.Sketch()
.circle(self.radius, mode='a')
.circle(self.radius_rider, mode='a')
.circle(self.radius_spring, mode='s')
.parray(
r=0,
a1=self.rider_slot_begin,
a1=rider_slot_begin,
da=self.rider_slot_span,
n=self.rider_n_slots)
.each(slot, mode='s')
@ -409,7 +421,7 @@ class TorsionJoint:
contact_profile
.parray(
r=0,
a1=self.rider_slot_begin,
a1=rider_slot_begin,
da=self.rider_slot_span,
n=self.rider_n_slots)
.each(slot, mode='s')
@ -420,7 +432,7 @@ class TorsionJoint:
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.radius,
radius=self.radius_rider,
height=self.rider_disk_height,
centered=(True, True, False))
.faces('>Z')
@ -442,16 +454,17 @@ class TorsionJoint:
#.workplane()
.hole(self.radius_axle * 2)
)
theta_begin = math.radians(self.rider_slot_begin) + math.pi
theta_begin = math.radians(rider_slot_begin)
theta_span = math.radians(self.rider_slot_span)
if abs(math.remainder(self.rider_slot_span, 360)) < TOL:
theta_step = theta_span / self.rider_n_slots
else:
theta_step = theta_span / (self.rider_n_slots - 1)
for i in range(self.rider_n_slots):
theta = theta_begin - i * theta_step
theta = theta_begin + i * theta_step
j = self.rider_n_slots - i - 1 if reverse_directrix_label else i
result.polyline(self._directrix(self.rider_disk_height, theta),
forConstruction=True).tag(f"directrix{i}")
forConstruction=True).tag(f"dir{j}")
return result
def rider_track_assembly(self, directrix=0):
@ -462,11 +475,26 @@ class TorsionJoint:
Cq.Assembly()
.add(spring, name="spring", color=Role.DAMPING.color)
.add(track, name="track", color=Role.PARENT.color)
.constrain("track?spring", "spring?top", "Plane")
.constrain("track?directrix", "spring?directrix_bot", "Axis")
.add(rider, name="rider", color=Role.CHILD.color)
.constrain("rider?spring", "spring?bot", "Plane")
.constrain(f"rider?directrix{directrix}", "spring?directrix_top", "Axis")
.solve()
.add(rider, name="rider", color=Role.PARENT.color)
)
TorsionJoint.add_constraints(result,
rider="rider", track="track", spring="spring",
directrix=directrix)
return result.solve()
@staticmethod
def add_constraints(assembly: Cq.Assembly,
rider: str, track: str, spring: str,
directrix: int = 0):
"""
Add the necessary constraints to a RT assembly
"""
(
assembly
.constrain(f"{track}?spring", f"{spring}?top", "Plane")
.constrain(f"{track}?dir", f"{spring}?dir_top",
"Axis", param=0)
.constrain(f"{rider}?spring", f"{spring}?bot", "Plane")
.constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot",
"Axis", param=0)
)
return result

View File

@ -46,11 +46,14 @@ def torsion_spring(radius=12,
centered=False)
)
r = -radius if right_handed else radius
result.polyline([(0, r, 0), (tail_length, r, 0)],
forConstruction=True).tag("directrix_bot")
c, s = math.cos(omega * math.pi / 180), math.sin(omega * math.pi / 180)
result.polyline([
(s * tail_length, c * r - s * tail_length, height),
(c * tail_length + s * r, c * r - s * tail_length, height)],
forConstruction=True).tag("directrix_top")
plane = result.copyWorkplane(Cq.Workplane('XY'))
plane.polyline([(0, r, 0), (tail_length, r, 0)],
forConstruction=True).tag("dir_bot")
omega = math.radians(omega)
c, s = math.cos(omega), math.sin(omega)
l = -tail_length
plane.polyline([
(-s * r, c * r, height),
(c * l - s * r, c * r + s * l, height)],
forConstruction=True).tag("dir_top")
return result

View File

@ -35,11 +35,13 @@ class TestJoints(unittest.TestCase):
def test_torsion_joint(self):
j = joints.TorsionJoint()
for slot in range(j.rider_n_slots):
with self.subTest(slot=slot):
with self.subTest(slot=slot, right_handed=False):
self.torsion_joint_case(j, slot)
def test_torsion_joint_right_handed(self):
j = joints.TorsionJoint(right_handed=True)
self.torsion_joint_case(j, 1)
for slot in range(j.rider_n_slots):
with self.subTest(slot=slot, right_handed=True):
self.torsion_joint_case(j, slot)
def test_torsion_joint_covered(self):
j = joints.TorsionJoint(
spring_hole_cover_track=True,

View File

@ -30,7 +30,6 @@ s1, s2, s3. The joints are named (from root to tip)
shoulder, elbow, wrist in analogy with human anatomy.
"""
from dataclasses import dataclass, field
import unittest
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly
@ -93,6 +92,9 @@ class Parameters(Model):
wing_root_wall_thickness: float = 8
shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=35,
radius_rider=35,
groove_radius_outer=32,
track_disk_height=5.0,
rider_disk_height=7.0,
radius_axle=8.0,
@ -114,6 +116,9 @@ class Parameters(Model):
wing_s1_thickness: float = 20
wing_s1_spacer_thickness: float = 25.4 / 8
wing_s1_spacer_width: float = 20
wing_s1_spacer_hole_diam: float = 8
wing_s1_shoulder_spacer_hole_dist: float = 20
wing_s1_shoulder_spacer_width: float = 60
trident_handle: Handle = field(default_factory=lambda: Handle(
diam=38,
@ -132,6 +137,8 @@ class Parameters(Model):
super().__init__(name="houjuu-nue")
assert self.wing_root_radius > self.hs_hirth_joint.radius,\
"Wing root must be large enough to accomodate joint"
assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \
"Spacer holes are too close to each other"
@target(name="trident/handle-connector")
def handle_connector(self):
@ -304,14 +311,32 @@ class Parameters(Model):
conn_thickness=self.wing_s0_thickness,
)
@target(name="shoulder_parent")
def shoulder_parent_joint(self) -> Cq.Workplane:
@target(name="shoulder_joint_parent")
def shoulder_joint_parent(self) -> Cq.Workplane:
joint = self.shoulder_torsion_joint
# Thickness of the lip connecting this joint to the wing root
lip_thickness = 10
lip_width = 25
lip_guard_ext = 40
lip_guard_height = self.wing_root_wall_thickness + lip_thickness
assert lip_guard_ext > joint.radius_track
lip_guard = (
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height)
.located(Cq.Location((0, -lip_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height))
)
result = (
self.shoulder_torsion_joint.rider()
joint.track()
.union(lip_guard, tol=1e-6)
# Extrude the handle
.copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness))))
.rect(25, 7, centered=(True, False))
.rect(lip_width, lip_thickness, centered=(True, False))
.extrude("next")
# Connector holes on the lip
.copyWorkplane(Cq.Workplane(
'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness))))
.hole(self.shoulder_attach_diam)
@ -322,27 +347,117 @@ class Parameters(Model):
result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1')
return result
@target(name="shoulder_child")
def shoulder_child_joint(self) -> Cq.Assembly:
# FIXME: half of conn_height
h = 100 / 2
dh = h - self.shoulder_torsion_joint.total_height
@target(name="shoulder_joint_child")
def shoulder_joint_child(self) -> Cq.Assembly:
"""
Creates the top/bottom shoulder child joint
"""
joint = self.shoulder_torsion_joint
# Half of the height of the bridging cylinder
dh = self.wing_s0_height / 2 - joint.total_height
core_start_angle = 30
core_end_angle1 = 90
core_end_angle2 = 180
core_thickness = 2
core_profile1 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core_profile2 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle))
.segment((0, 0))
.close()
.assemble()
.circle(joint.radius_rider - core_thickness, mode='s')
)
core = (
Cq.Workplane('XY')
.moveTo(0, 15)
.box(50, 40, 2 * dh, centered=(True, False, True))
.placeSketch(core_profile1)
.toPending()
.extrude(dh * 2)
.copyWorkplane(Cq.Workplane('XY'))
.placeSketch(core_profile2)
.toPending()
.extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh))
)
# Create the upper and lower lips
lip_height = self.wing_s1_thickness
lip_thickness = joint.rider_disk_height
lip_ext = 40 + joint.radius_rider
hole_dx = self.wing_s1_shoulder_spacer_hole_dist
assert lip_height / 2 <= joint.radius_rider
lip = (
Cq.Workplane('XY')
.box(lip_ext, lip_height, lip_thickness,
centered=(False, True, False))
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(radius=joint.radius_rider, height=lip_thickness,
centered=(True, True, False),
combine='cut')
.faces(">Z")
.workplane()
)
hole_x = lip_ext - hole_dx / 2
for i in range(2):
plane = (
lip
.moveTo(hole_x - i * hole_dx, 0)
)
lip = plane.hole(self.wing_s1_spacer_hole_diam)
plane.tagPlane(f"hole{i}")
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
result = (
Cq.Assembly()
.add(core, name="core", loc=Cq.Location())
.add(self.shoulder_torsion_joint.track(), name="track_top",
.add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top",
loc=Cq.Location((0, 0, dh), (0, 0, 1), -90))
.add(self.shoulder_torsion_joint.track(), name="track_bot",
.add(joint.rider(rider_slot_begin=180), name="rider_bot",
loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
.add(lip, name="lip_top",
loc=Cq.Location((0, 0, dh)))
.add(lip, name="lip_bot",
loc=Cq.Location((0, 0, -dh)) * loc_rotate)
)
return result
@assembly()
def shoulder_assembly(self) -> Cq.Assembly:
directrix = 0
result = (
Cq.Assembly()
.add(self.shoulder_joint_child(), name="child",
color=Role.CHILD.color)
.constrain("child/core", "Fixed")
.add(self.shoulder_torsion_joint.spring(), name="spring_top",
color=Role.DAMPING.color)
.add(self.shoulder_joint_parent(), name="parent_top",
color=Role.PARENT.color)
.add(self.shoulder_torsion_joint.spring(), name="spring_bot",
color=Role.DAMPING.color)
.add(self.shoulder_joint_parent(), name="parent_bot",
color=Role.PARENT.color)
)
TorsionJoint.add_constraints(result,
rider="child/rider_top",
track="parent_top",
spring="spring_top",
directrix=directrix)
TorsionJoint.add_constraints(result,
rider="child/rider_bot",
track="parent_bot",
spring="spring_bot",
directrix=directrix)
return result.solve()
@target(name="wing/s1-spacer", kind=TargetKind.DXF)
def wing_s1_spacer(self) -> Cq.Workplane:
result = (
@ -357,6 +472,36 @@ class Parameters(Model):
result.faces(">Y").tag("dir")
return result
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
def wing_s1_shoulder_spacer(self) -> Cq.Workplane:
dx = self.wing_s1_shoulder_spacer_hole_dist
result = (
Cq.Workplane('XZ')
.sketch()
.rect(self.wing_s1_shoulder_spacer_width,
self.wing_s1_thickness)
.push([
(0, 0),
(dx, 0),
])
.circle(self.wing_s1_spacer_hole_diam / 2, mode='s')
.finalize()
.extrude(self.wing_s1_spacer_thickness)
)
# Tag the mating surfaces to be glued
result.faces("<Z").tag("mate1")
result.faces(">Z").tag("mate2")
# Tag the directrix
result.faces(">Y").tag("dir")
# Tag the holes
plane = result.faces(">Y").workplane()
# Side closer to the parent is 0
plane.moveTo(dx, 0).tagPlane("hole0")
plane.tagPlane("hole1")
return result
@target(name="wing/r1s1", kind=TargetKind.DXF)
def wing_r1s1_profile(self) -> Cq.Sketch:
return MW.wing_r1s1_profile()
@ -379,35 +524,6 @@ class Parameters(Model):
plane.moveTo(px, sign * py).tagPlane(name)
return result
@assembly()
def shoulder_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.shoulder_child_joint(), name="child",
color=Role.CHILD.color)
.constrain("child/core", "Fixed")
# Top parent joint
.add(self.shoulder_torsion_joint.spring(), name="spring_top",
color=Role.DAMPING.color)
.constrain("child/track_top?spring", "spring_top?top", "Plane")
.constrain("child/track_top?directrix", "spring_top?directrix_bot", "Axis")
.add(self.shoulder_parent_joint(), name="parent_top",
color=Role.PARENT.color)
.constrain("parent_top?spring", "spring_top?bot", "Plane")
.constrain("parent_top?directrix0", "spring_top?directrix_top", "Axis")
# Bottom parent joint
.add(self.shoulder_torsion_joint.spring(), name="spring_bot",
color=Role.DAMPING.color)
.constrain("child/track_bot?spring", "spring_bot?top", "Plane")
.constrain("child/track_bot?directrix", "spring_bot?directrix_bot", "Axis")
.add(self.shoulder_parent_joint(), name="parent_bot",
color=Role.PARENT.color)
.constrain("parent_bot?spring", "spring_bot?bot", "Plane")
.constrain("parent_bot?directrix0", "spring_bot?directrix_top", "Axis")
.solve()
)
return result
@assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly:
result = (
@ -427,6 +543,13 @@ class Parameters(Model):
.constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane")
.constrain(f"panel_back?{tag}", f"{tag}_spacer?mate2", "Plane")
)
(
result
.add(self.shoulder_assembly(), name="shoulder")
.constrain("shoulder_bot_spacer?dir",
"shoulder/child/core?mate_bot",
"Plane")
)
result.solve()
return result