cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
5 changed files with 102 additions and 30 deletions
Showing only changes of commit 54593b9a4e - Show all commits

View File

@ -379,7 +379,9 @@ class TorsionJoint:
forConstruction=True).tag("directrix") forConstruction=True).tag("directrix")
return result return result
def rider(self): def rider(self, rider_slot_begin=None):
if not rider_slot_begin:
rider_slot_begin = self.rider_slot_begin
def slot(loc): def slot(loc):
wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False)) wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False))
face = Cq.Face.makeFromWires(wire) face = Cq.Face.makeFromWires(wire)

View File

@ -24,6 +24,7 @@ class TestJoints(unittest.TestCase):
joints.comma_assembly() joints.comma_assembly()
def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int): def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int):
assert 0 <= slot and slot < joint.rider_n_slots
assembly = joint.rider_track_assembly(slot) assembly = joint.rider_track_assembly(slot)
bbox = assembly.toCompound().BoundingBox() bbox = assembly.toCompound().BoundingBox()
self.assertAlmostEqual(bbox.zlen, joint.total_height) self.assertAlmostEqual(bbox.zlen, joint.total_height)
@ -45,6 +46,11 @@ class TestJoints(unittest.TestCase):
spring_hole_cover_rider=True, spring_hole_cover_rider=True,
) )
self.torsion_joint_case(j, 1) self.torsion_joint_case(j, 1)
def test_torsion_joint_slot(self):
j = joints.TorsionJoint(
rider_slot_begin=90,
)
self.torsion_joint_case(j, 1)

View File

@ -92,8 +92,10 @@ class Parameters(Model):
wing_root_radius: float = 40 wing_root_radius: float = 40
wing_root_wall_thickness: float = 8 wing_root_wall_thickness: float = 8
shoulder_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_axle=8 track_disk_height=5.0,
rider_disk_height=7.0,
radius_axle=8.0,
)) ))
# Two holes on each side (top and bottom) are used to attach the shoulder # Two holes on each side (top and bottom) are used to attach the shoulder
@ -104,10 +106,10 @@ class Parameters(Model):
""" """
Heights for various wing joints, where the numbers start from the first joint. Heights for various wing joints, where the numbers start from the first joint.
""" """
wing_s0_thickness: float = 40
wing_s0_height: float = 100
wing_r1_height: float = 100 wing_r1_height: float = 100
wing_r1_width: float = 400 wing_r1_width: float = 400
wing_r2_height: float = 100
wing_r3_height: float = 100
trident_handle: Handle = field(default_factory=lambda: Handle( trident_handle: Handle = field(default_factory=lambda: Handle(
diam=38, diam=38,
@ -267,18 +269,20 @@ class Parameters(Model):
shoulder_attach_dist=self.shoulder_attach_dist, shoulder_attach_dist=self.shoulder_attach_dist,
shoulder_attach_diam=self.shoulder_attach_diam, shoulder_attach_diam=self.shoulder_attach_diam,
wall_thickness=self.wing_root_wall_thickness, wall_thickness=self.wing_root_wall_thickness,
conn_height=self.wing_s0_height,
conn_thickness=self.wing_s0_thickness,
) )
@target(name="shoulder") @target(name="shoulder_parent")
def shoulder_parent_joint(self) -> Cq.Assembly: def shoulder_parent_joint(self) -> Cq.Workplane:
result = ( result = (
self.shoulder_joint.rider() self.shoulder_torsion_joint.rider()
.copyWorkplane(Cq.Workplane( .copyWorkplane(Cq.Workplane(
'YZ', origin=Cq.Vector((100, 0, self.wing_root_wall_thickness)))) 'YZ', origin=Cq.Vector((90, 0, self.wing_root_wall_thickness))))
.rect(30, 10, centered=(True, False)) .rect(25, 7, centered=(True, False))
.extrude("next") .extrude("next")
.copyWorkplane(Cq.Workplane( .copyWorkplane(Cq.Workplane(
'YX', origin=Cq.Vector((60, 0, self.wing_root_wall_thickness)))) 'YX', origin=Cq.Vector((55, 0, self.wing_root_wall_thickness))))
.hole(self.shoulder_attach_diam) .hole(self.shoulder_attach_diam)
.moveTo(0, self.shoulder_attach_dist) .moveTo(0, self.shoulder_attach_dist)
.hole(self.shoulder_attach_diam) .hole(self.shoulder_attach_diam)
@ -287,6 +291,27 @@ 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")
def shoulder_child_joint(self) -> Cq.Assembly:
# FIXME: half of conn_height
h = 100 / 2
dh = h - self.shoulder_torsion_joint.total_height
core = (
Cq.Workplane('XY')
.moveTo(0, 15)
.box(50, 40, 2 * dh, centered=(True, False, True))
)
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",
loc=Cq.Location((0, 0, dh), (0, 0, 1), -90))
.add(self.shoulder_torsion_joint.track(), name="track_bot",
loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
)
return result
def wing_r1_profile(self) -> Cq.Sketch: def wing_r1_profile(self) -> Cq.Sketch:
""" """
Generates the first wing segment profile, with the wing root pointing in Generates the first wing segment profile, with the wing root pointing in
@ -352,14 +377,44 @@ class Parameters(Model):
result.solve() result.solve()
return result return result
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
def wing_r1_assembly(self) -> Cq.Assembly: def wing_r1_assembly(self) -> Cq.Assembly:
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(self.wing_root(), name="r1") .add(self.wing_root(), name="r1")
.add(self.shoulder_parent_joint(), name="shoulder_parent_top", .add(self.shoulder_assembly(), name="shoulder")
color=Material.RESIN_TRANSPARENT.color) .constrain("r1/scaffold", "Fixed")
.constrain("r1/scaffold?conn_top0", "shoulder_parent_top?conn0", "Plane") .constrain("r1/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane")
.constrain("r1/scaffold?conn_top1", "shoulder_parent_top?conn1", "Plane") .constrain("r1/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane")
.constrain("r1/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane")
.constrain("r1/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane")
.solve() .solve()
) )
return result return result

View File

@ -1,6 +1,7 @@
import unittest import unittest
import cadquery as Cq import cadquery as Cq
import nhf.touhou.houjuu_nue as M import nhf.touhou.houjuu_nue as M
from nhf.checks import pairwise_intersection
class Test(unittest.TestCase): class Test(unittest.TestCase):
@ -8,19 +9,23 @@ class Test(unittest.TestCase):
p = M.Parameters() p = M.Parameters()
obj = p.hs_joint_parent() obj = p.hs_joint_parent()
self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece")
def test_shoulder_joint(self):
p = M.Parameters()
shoulder = p.shoulder_assembly()
assert isinstance(shoulder, Cq.Assembly)
self.assertEqual(pairwise_intersection(shoulder), [])
def test_wing_root(self): def test_wing_root(self):
p = M.Parameters() p = M.Parameters()
obj = p.wing_root() obj = p.wing_root()
assert isinstance(obj, Cq.Assembly)
#self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece")
bbox = obj.val().BoundingBox() bbox = obj.toCompound().BoundingBox()
msg = "Must fix 256^3 bbox" msg = "Must fix 256^3 bbox"
self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.xlen, 255, msg=msg)
self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg)
self.assertLess(bbox.zlen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg)
def test_wing_root(self):
p = M.Parameters()
p.wing_root()
def test_wings_assembly(self): def test_wings_assembly(self):
p = M.Parameters() p = M.Parameters()
p.wings_assembly() p.wings_assembly()

View File

@ -14,7 +14,7 @@ def wing_root_profiles(
base_radius=40, base_radius=40,
middle_offset=30, middle_offset=30,
middle_height=80, middle_height=80,
conn_width=40, conn_thickness=40,
conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: conn_height=100) -> tuple[Cq.Wire, Cq.Wire]:
assert base_sweep < 180 assert base_sweep < 180
assert middle_offset > 0 assert middle_offset > 0
@ -74,7 +74,7 @@ def wing_root_profiles(
# If the exterior sweep is theta', it has to satisfy # If the exterior sweep is theta', it has to satisfy
# #
# sin(theta) * r2 + wall_thickness = sin(theta') * r1 # sin(theta) * r2 + wall_thickness = sin(theta') * r1
x, y = conn_width / 2, middle_height / 2 x, y = conn_thickness / 2, middle_height / 2
t = wall_thickness t = wall_thickness
dx = middle_offset dx = middle_offset
middle = ( middle = (
@ -109,7 +109,7 @@ def wing_root_profiles(
) )
assert isinstance(middle, Cq.Wire) assert isinstance(middle, Cq.Wire)
x, y = conn_width / 2, conn_height / 2 x, y = conn_thickness / 2, conn_height / 2
t = wall_thickness t = wall_thickness
tip = ( tip = (
Cq.Sketch() Cq.Sketch()
@ -132,30 +132,32 @@ def wing_root(joint: HirthJoint,
union_tol=1e-4, union_tol=1e-4,
shoulder_attach_diam=8, shoulder_attach_diam=8,
shoulder_attach_dist=25, shoulder_attach_dist=25,
conn_width=40, conn_thickness=40,
conn_height=100, conn_height=100,
wall_thickness=8) -> Cq.Assembly: wall_thickness=8) -> Cq.Assembly:
""" """
Generate the contiguous components of the root wing segment Generate the contiguous components of the root wing segment
""" """
tip_centre = Cq.Vector((-150, 0, -80)) tip_centre = Cq.Vector((-150, 0, -80))
attach_theta = math.radians(5)
c, s = math.cos(attach_theta), math.sin(attach_theta)
attach_points = [ attach_points = [
(15, 0), (15, 4),
(15 + shoulder_attach_dist, 0), (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s),
] ]
root_profile, middle_profile, tip_profile = wing_root_profiles( root_profile, middle_profile, tip_profile = wing_root_profiles(
conn_width=conn_width, conn_thickness=conn_thickness,
conn_height=conn_height, conn_height=conn_height,
wall_thickness=8, wall_thickness=8,
) )
middle_profile = middle_profile.located(Cq.Location( middle_profile = middle_profile.located(Cq.Location(
(-40, 0, -40), (0, 30, 0) (-40, 0, -40), (0, 1, 0), 30
)) ))
antetip_profile = tip_profile.located(Cq.Location( antetip_profile = tip_profile.located(Cq.Location(
(-95, 0, -75), (0, 60, 0) (-95, 0, -75), (0, 1, 0), 60
)) ))
tip_profile = tip_profile.located(Cq.Location( tip_profile = tip_profile.located(Cq.Location(
tip_centre, (0, 90, 0) tip_centre, (0, 1, 0), 90
)) ))
profiles = [ profiles = [
root_profile, root_profile,
@ -201,9 +203,11 @@ def wing_root(joint: HirthJoint,
Cq.Vector((0, y, 0))) Cq.Vector((0, y, 0)))
) )
) )
if side == "bottom":
side = "bot"
for i, (px, py) in enumerate(attach_points): for i, (px, py) in enumerate(attach_points):
tag = f"conn_{side}{i}" tag = f"conn_{side}{i}"
plane.moveTo(px, py).tagPlane(tag) plane.moveTo(px, -py if side == "top" else py).tagPlane(tag)
result.faces("<Z").tag("base") result.faces("<Z").tag("base")
result.faces(">X").tag("conn") result.faces(">X").tag("conn")