cosplay: Touhou/Houjuu Nue #4
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue