Cosplay/nhf/touhou/houjuu_nue/wing.py

373 lines
10 KiB
Python

"""
This file describes the shapes of the wing shells. The joints are defined in
`__init__.py`.
"""
import math
from dataclasses import dataclass
import cadquery as Cq
from nhf import Material, Role
from nhf.parts.joints import HirthJoint
import nhf.utils
def wing_root_profiles(
base_sweep=150,
wall_thickness=8,
base_radius=40,
middle_offset=30,
middle_height=80,
conn_thickness=40,
conn_height=100) -> tuple[Cq.Wire, Cq.Wire]:
assert base_sweep < 180
assert middle_offset > 0
theta = math.pi * base_sweep / 180
c, s = math.cos(theta), math.sin(theta)
c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75)
c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2)
r1 = base_radius
r2 = base_radius - wall_thickness
base = (
Cq.Sketch()
.arc(
(c * r1, s * r1),
(c_1 * r1, s_1 * r1),
(c_2 * r1, s_2 * r1),
)
.arc(
(c_2 * r1, s_2 * r1),
(r1, 0),
(c_2 * r1, -s_2 * r1),
)
.arc(
(c_2 * r1, -s_2 * r1),
(c_1 * r1, -s_1 * r1),
(c * r1, -s * r1),
)
.segment(
(c * r1, -s * r1),
(c * r2, -s * r2),
)
.arc(
(c * r2, -s * r2),
(c_1 * r2, -s_1 * r2),
(c_2 * r2, -s_2 * r2),
)
.arc(
(c_2 * r2, -s_2 * r2),
(r2, 0),
(c_2 * r2, s_2 * r2),
)
.arc(
(c_2 * r2, s_2 * r2),
(c_1 * r2, s_1 * r2),
(c * r2, s * r2),
)
.segment(
(c * r2, s * r2),
(c * r1, s * r1),
)
.assemble(tag="wire")
.wires().val()
)
assert isinstance(base, Cq.Wire)
# The interior sweep is given by theta, but the exterior sweep exceeds the
# interior sweep so the wall does not become thinner towards the edges.
# If the exterior sweep is theta', it has to satisfy
#
# sin(theta) * r2 + wall_thickness = sin(theta') * r1
x, y = conn_thickness / 2, middle_height / 2
t = wall_thickness
dx = middle_offset
middle = (
Cq.Sketch()
# Interior arc, top point
.arc(
(x - t, y - t),
(x - t + dx, 0),
(x - t, -y + t),
)
.segment(
(x - t, -y + t),
(-x, -y+t)
)
.segment((-x, -y))
.segment((x, -y))
# Outer arc, bottom point
.arc(
(x, -y),
(x + dx, 0),
(x, y),
)
.segment(
(x, y),
(-x, y)
)
.segment((-x, y-t))
#.segment((x2, a))
.close()
.assemble(tag="wire")
.wires().val()
)
assert isinstance(middle, Cq.Wire)
x, y = conn_thickness / 2, conn_height / 2
t = wall_thickness
tip = (
Cq.Sketch()
.segment((-x, y), (x, y))
.segment((x, -y))
.segment((-x, -y))
.segment((-x, -y+t))
.segment((x-t, -y+t))
.segment((x-t, y-t))
.segment((-x, y-t))
.close()
.assemble(tag="wire")
.wires().val()
)
return base, middle, tip
def wing_root(joint: HirthJoint,
bolt_diam: int = 12,
union_tol=1e-4,
shoulder_attach_diam=8,
shoulder_attach_dist=25,
conn_thickness=40,
conn_height=100,
wall_thickness=8) -> Cq.Assembly:
"""
Generate the contiguous components of the root wing segment
"""
tip_centre = Cq.Vector((-150, 0, -80))
attach_theta = math.radians(5)
c, s = math.cos(attach_theta), math.sin(attach_theta)
attach_points = [
(15, 4),
(15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s),
]
root_profile, middle_profile, tip_profile = wing_root_profiles(
conn_thickness=conn_thickness,
conn_height=conn_height,
wall_thickness=8,
)
middle_profile = middle_profile.located(Cq.Location(
(-40, 0, -40), (0, 1, 0), 30
))
antetip_profile = tip_profile.located(Cq.Location(
(-95, 0, -75), (0, 1, 0), 60
))
tip_profile = tip_profile.located(Cq.Location(
tip_centre, (0, 1, 0), 90
))
profiles = [
root_profile,
middle_profile,
antetip_profile,
tip_profile,
]
result = None
for p1, p2 in zip(profiles[:-1], profiles[1:]):
seg = (
Cq.Workplane('XY')
.add(p1)
.toPending()
.workplane() # This call is necessary
.add(p2)
.toPending()
.loft()
)
if result:
result = result.union(seg, tol=union_tol)
else:
result = seg
result = (
result
# Create connector holes
.copyWorkplane(
Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0)))
)
.pushPoints(attach_points)
.hole(shoulder_attach_diam)
)
# Generate attach point tags
for sign in [False, True]:
y = conn_height / 2 - wall_thickness
side = "bottom" if sign else "top"
y = y if sign else -y
plane = (
result
# Create connector holes
.copyWorkplane(
Cq.Workplane(side, origin=tip_centre +
Cq.Vector((0, y, 0)))
)
)
if side == "bottom":
side = "bot"
for i, (px, py) in enumerate(attach_points):
tag = f"conn_{side}{i}"
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag)
result.faces("<Z").tag("base")
result.faces(">X").tag("conn")
j = (
joint.generate(is_mated=True)
.faces("<Z")
.hole(bolt_diam)
)
color = Material.PLASTIC_PLA.color
result = (
Cq.Assembly()
.add(result, name="scaffold", color=color)
.add(j, name="joint", color=Role.CHILD.color,
loc=Cq.Location((0, 0, -joint.total_height)))
)
return result
@dataclass
class WingProfile:
shoulder_height: float = 100
elbow_height: float = 120
elbow_x: float = 270
elbow_y: float = 10
# Angle of elbow w.r.t. y axis
elbow_angle: float = -20
wrist_height: float = 70
# Bottom point of the wrist
wrist_x: float = 400
wrist_y: float = 200
# Angle of wrist w.r.t. y axis. should be negative
wrist_angle: float = -40
# Extends from the wrist to the tip of the arrow
arrow_height: float = 300
arrow_angle: float = 7
# Relative (in wrist coordinate) centre of the ring
ring_x: float = 40
ring_y: float = 20
ring_radius_inner: float = 22
def __post_init__(self):
assert self.ring_radius > self.ring_radius_inner
@property
def ring_radius(self) -> float:
dx = self.ring_x
dy = self.ring_y
return (dx * dx + dy * dy) ** 0.5
def wing_r1_profile(self) -> Cq.Sketch:
wrist_theta = math.radians(self.wrist_angle)
wrist_s = math.sin(wrist_theta)
wrist_c = math.cos(wrist_theta)
wrist_top_x = self.wrist_x + self.wrist_height * wrist_s
wrist_top_y = self.wrist_y + self.wrist_height * wrist_c
elbow_theta = math.radians(self.elbow_angle)
elbow_top_x = self.elbow_x + self.elbow_height * math.sin(elbow_theta)
elbow_top_y = self.elbow_y + self.elbow_height * math.cos(elbow_theta)
result = (
Cq.Sketch()
.segment(
(0, 0),
(0, self.shoulder_height),
tag="shoulder")
.arc(
(0, self.shoulder_height),
(elbow_top_x, elbow_top_y),
(wrist_top_x, wrist_top_y),
tag="s1_top")
#.segment(
# (self.wrist_x, self.wrist_y),
# (wrist_top_x, wrist_top_y),
# tag="wrist")
.arc(
(0, 0),
(self.elbow_x, self.elbow_y),
(self.wrist_x, self.wrist_y),
tag="s1_bot")
)
arrow_theta = math.radians(self.arrow_angle)
arrow_x = self.wrist_x - self.arrow_height * wrist_s
arrow_y = self.wrist_y - self.arrow_height * wrist_c
arrow_tip_x = arrow_x + (self.arrow_height + self.wrist_height) * math.sin(arrow_theta + wrist_theta)
arrow_tip_y = arrow_y + (self.arrow_height + self.wrist_height) * math.cos(arrow_theta + wrist_theta)
result = (
result
.segment(
(self.wrist_x, self.wrist_y),
(arrow_x, arrow_y)
)
.segment(
(arrow_x, arrow_y),
(arrow_tip_x, arrow_tip_y)
)
.segment(
(arrow_tip_x, arrow_tip_y),
(wrist_top_x, wrist_top_y)
)
)
# Carve out the ring
result = result.assemble()
ring_x = wrist_top_x + wrist_c * self.ring_x + wrist_s * self.ring_y
ring_y = wrist_top_y - wrist_s * self.ring_x + wrist_c * self.ring_y
result = (
result
.push([(ring_x, ring_y)])
.circle(self.ring_radius, mode='a')
.circle(self.ring_radius_inner, mode='s')
.clean()
)
return result
def wing_r1s1_profile(self) -> Cq.Sketch:
"""
Generates the first wing segment profile, with the wing root pointing in
the positive x axis.
"""
w = 270
# Depression of the wing middle, measured
h = 0
# spline curve easing extension
theta = math.radians(30)
c_th, s_th = math.cos(theta), math.sin(theta)
bend = 30
ext = 40
ext_dh = -5
assert ext * 2 < w
factor = 0.7
result = (
Cq.Sketch()
.segment((0, 0), (0, self.shoulder_height))
.spline([
(0, self.shoulder_height),
((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend),
(w - s_th * self.elbow_height, self.elbow_height * c_th - h),
])
.segment(
(w - s_th * self.elbow_height, self.elbow_height * c_th -h),
(w, -h),
)
.spline([
(0, 0),
(w / 2, -h / 2 - bend),
(w, -h),
])
.assemble()
)
return result