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

View File

@ -35,11 +35,13 @@ class TestJoints(unittest.TestCase):
def test_torsion_joint(self): def test_torsion_joint(self):
j = joints.TorsionJoint() j = joints.TorsionJoint()
for slot in range(j.rider_n_slots): 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) self.torsion_joint_case(j, slot)
def test_torsion_joint_right_handed(self): def test_torsion_joint_right_handed(self):
j = joints.TorsionJoint(right_handed=True) 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): def test_torsion_joint_covered(self):
j = joints.TorsionJoint( j = joints.TorsionJoint(
spring_hole_cover_track=True, 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. shoulder, elbow, wrist in analogy with human anatomy.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
import unittest
import cadquery as Cq 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
@ -93,6 +92,9 @@ class Parameters(Model):
wing_root_wall_thickness: float = 8 wing_root_wall_thickness: float = 8
shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=35,
radius_rider=35,
groove_radius_outer=32,
track_disk_height=5.0, track_disk_height=5.0,
rider_disk_height=7.0, rider_disk_height=7.0,
radius_axle=8.0, radius_axle=8.0,
@ -114,6 +116,9 @@ class Parameters(Model):
wing_s1_thickness: float = 20 wing_s1_thickness: float = 20
wing_s1_spacer_thickness: float = 25.4 / 8 wing_s1_spacer_thickness: float = 25.4 / 8
wing_s1_spacer_width: float = 20 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( trident_handle: Handle = field(default_factory=lambda: Handle(
diam=38, diam=38,
@ -132,6 +137,8 @@ class Parameters(Model):
super().__init__(name="houjuu-nue") super().__init__(name="houjuu-nue")
assert self.wing_root_radius > self.hs_hirth_joint.radius,\ assert self.wing_root_radius > self.hs_hirth_joint.radius,\
"Wing root must be large enough to accomodate joint" "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") @target(name="trident/handle-connector")
def handle_connector(self): def handle_connector(self):
@ -304,14 +311,32 @@ class Parameters(Model):
conn_thickness=self.wing_s0_thickness, conn_thickness=self.wing_s0_thickness,
) )
@target(name="shoulder_parent") @target(name="shoulder_joint_parent")
def shoulder_parent_joint(self) -> Cq.Workplane: 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 = ( result = (
self.shoulder_torsion_joint.rider() joint.track()
.union(lip_guard, tol=1e-6)
# Extrude the handle
.copyWorkplane(Cq.Workplane( .copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) '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") .extrude("next")
# Connector holes on the lip
.copyWorkplane(Cq.Workplane( .copyWorkplane(Cq.Workplane(
'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness))))
.hole(self.shoulder_attach_diam) .hole(self.shoulder_attach_diam)
@ -322,27 +347,117 @@ class Parameters(Model):
result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1')
return result return result
@target(name="shoulder_child") @target(name="shoulder_joint_child")
def shoulder_child_joint(self) -> Cq.Assembly: def shoulder_joint_child(self) -> Cq.Assembly:
# FIXME: half of conn_height """
h = 100 / 2 Creates the top/bottom shoulder child joint
dh = h - self.shoulder_torsion_joint.total_height """
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 = ( core = (
Cq.Workplane('XY') Cq.Workplane('XY')
.moveTo(0, 15) .placeSketch(core_profile1)
.box(50, 40, 2 * dh, centered=(True, False, True)) .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) loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(core, name="core", loc=Cq.Location()) .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)) 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) 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 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) @target(name="wing/s1-spacer", kind=TargetKind.DXF)
def wing_s1_spacer(self) -> Cq.Workplane: def wing_s1_spacer(self) -> Cq.Workplane:
result = ( result = (
@ -357,6 +472,36 @@ class Parameters(Model):
result.faces(">Y").tag("dir") result.faces(">Y").tag("dir")
return result 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) @target(name="wing/r1s1", kind=TargetKind.DXF)
def wing_r1s1_profile(self) -> Cq.Sketch: def wing_r1s1_profile(self) -> Cq.Sketch:
return MW.wing_r1s1_profile() return MW.wing_r1s1_profile()
@ -379,35 +524,6 @@ class Parameters(Model):
plane.moveTo(px, sign * py).tagPlane(name) plane.moveTo(px, sign * py).tagPlane(name)
return result 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() @assembly()
def wing_r1s1_assembly(self) -> Cq.Assembly: def wing_r1s1_assembly(self) -> Cq.Assembly:
result = ( result = (
@ -427,6 +543,13 @@ class Parameters(Model):
.constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane") .constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane")
.constrain(f"panel_back?{tag}", f"{tag}_spacer?mate2", "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() result.solve()
return result return result