2024-06-28 20:07:37 -07:00
|
|
|
"""
|
|
|
|
This file describes the shapes of the wing shells. The joints are defined in
|
|
|
|
`__init__.py`.
|
|
|
|
"""
|
2024-06-24 16:13:15 -07:00
|
|
|
import math
|
2024-07-09 19:57:54 -07:00
|
|
|
from dataclasses import dataclass
|
2024-06-24 16:13:15 -07:00
|
|
|
import cadquery as Cq
|
2024-07-04 12:03:38 -07:00
|
|
|
from nhf import Material, Role
|
2024-07-04 00:42:14 -07:00
|
|
|
from nhf.parts.joints import HirthJoint
|
2024-07-06 16:41:13 -07:00
|
|
|
import nhf.utils
|
2024-06-24 16:13:15 -07:00
|
|
|
|
2024-07-09 19:57:54 -07:00
|
|
|
|
2024-06-24 16:13:15 -07:00
|
|
|
def wing_root_profiles(
|
|
|
|
base_sweep=150,
|
|
|
|
wall_thickness=8,
|
2024-07-04 17:50:11 -07:00
|
|
|
base_radius=40,
|
2024-06-24 16:13:15 -07:00
|
|
|
middle_offset=30,
|
2024-07-04 17:50:11 -07:00
|
|
|
middle_height=80,
|
2024-07-07 12:15:47 -07:00
|
|
|
conn_thickness=40,
|
2024-06-24 16:13:15 -07:00
|
|
|
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
|
2024-07-07 12:15:47 -07:00
|
|
|
x, y = conn_thickness / 2, middle_height / 2
|
2024-06-24 16:13:15 -07:00
|
|
|
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)
|
|
|
|
|
2024-07-07 12:15:47 -07:00
|
|
|
x, y = conn_thickness / 2, conn_height / 2
|
2024-06-24 16:13:15 -07:00
|
|
|
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
|
2024-06-28 20:07:37 -07:00
|
|
|
|
|
|
|
|
|
|
|
def wing_root(joint: HirthJoint,
|
2024-07-04 17:50:11 -07:00
|
|
|
bolt_diam: int = 12,
|
|
|
|
union_tol=1e-4,
|
2024-07-06 16:41:13 -07:00
|
|
|
shoulder_attach_diam=8,
|
|
|
|
shoulder_attach_dist=25,
|
2024-07-07 12:15:47 -07:00
|
|
|
conn_thickness=40,
|
2024-07-04 17:50:11 -07:00
|
|
|
conn_height=100,
|
|
|
|
wall_thickness=8) -> Cq.Assembly:
|
2024-06-28 20:07:37 -07:00
|
|
|
"""
|
|
|
|
Generate the contiguous components of the root wing segment
|
|
|
|
"""
|
2024-07-04 17:50:11 -07:00
|
|
|
tip_centre = Cq.Vector((-150, 0, -80))
|
2024-07-07 12:15:47 -07:00
|
|
|
attach_theta = math.radians(5)
|
|
|
|
c, s = math.cos(attach_theta), math.sin(attach_theta)
|
2024-07-04 17:50:11 -07:00
|
|
|
attach_points = [
|
2024-07-07 12:15:47 -07:00
|
|
|
(15, 4),
|
|
|
|
(15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s),
|
2024-07-04 17:50:11 -07:00
|
|
|
]
|
|
|
|
root_profile, middle_profile, tip_profile = wing_root_profiles(
|
2024-07-07 12:15:47 -07:00
|
|
|
conn_thickness=conn_thickness,
|
2024-07-04 17:50:11 -07:00
|
|
|
conn_height=conn_height,
|
|
|
|
wall_thickness=8,
|
2024-06-27 20:22:54 -07:00
|
|
|
)
|
2024-07-04 17:50:11 -07:00
|
|
|
middle_profile = middle_profile.located(Cq.Location(
|
2024-07-07 12:15:47 -07:00
|
|
|
(-40, 0, -40), (0, 1, 0), 30
|
2024-07-04 17:50:11 -07:00
|
|
|
))
|
|
|
|
antetip_profile = tip_profile.located(Cq.Location(
|
2024-07-07 12:15:47 -07:00
|
|
|
(-95, 0, -75), (0, 1, 0), 60
|
2024-07-04 17:50:11 -07:00
|
|
|
))
|
|
|
|
tip_profile = tip_profile.located(Cq.Location(
|
2024-07-07 12:15:47 -07:00
|
|
|
tip_centre, (0, 1, 0), 90
|
2024-07-04 17:50:11 -07:00
|
|
|
))
|
|
|
|
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)
|
2024-07-06 16:41:13 -07:00
|
|
|
.hole(shoulder_attach_diam)
|
2024-06-24 16:13:15 -07:00
|
|
|
)
|
2024-07-04 17:50:11 -07:00
|
|
|
# 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)))
|
|
|
|
)
|
|
|
|
)
|
2024-07-07 12:15:47 -07:00
|
|
|
if side == "bottom":
|
|
|
|
side = "bot"
|
2024-07-04 17:50:11 -07:00
|
|
|
for i, (px, py) in enumerate(attach_points):
|
2024-07-06 16:41:13 -07:00
|
|
|
tag = f"conn_{side}{i}"
|
2024-07-07 12:15:47 -07:00
|
|
|
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag)
|
2024-07-04 17:50:11 -07:00
|
|
|
|
2024-06-27 20:22:54 -07:00
|
|
|
result.faces("<Z").tag("base")
|
|
|
|
result.faces(">X").tag("conn")
|
2024-06-28 20:07:37 -07:00
|
|
|
|
|
|
|
j = (
|
|
|
|
joint.generate(is_mated=True)
|
|
|
|
.faces("<Z")
|
|
|
|
.hole(bolt_diam)
|
|
|
|
)
|
2024-07-04 12:03:38 -07:00
|
|
|
|
|
|
|
color = Material.PLASTIC_PLA.color
|
2024-06-28 20:07:37 -07:00
|
|
|
result = (
|
2024-07-04 12:03:38 -07:00
|
|
|
Cq.Assembly()
|
|
|
|
.add(result, name="scaffold", color=color)
|
|
|
|
.add(j, name="joint", color=Role.CHILD.color,
|
|
|
|
loc=Cq.Location((0, 0, -joint.total_height)))
|
2024-06-28 20:07:37 -07:00
|
|
|
)
|
2024-06-27 20:22:54 -07:00
|
|
|
return result
|
2024-07-07 21:01:40 -07:00
|
|
|
|
2024-07-09 19:57:54 -07:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class WingProfile:
|
|
|
|
|
|
|
|
shoulder_height: float = 100
|
|
|
|
elbow_height: float = 120
|
|
|
|
|
|
|
|
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()
|
2024-07-07 21:01:40 -07:00
|
|
|
)
|
2024-07-09 19:57:54 -07:00
|
|
|
return result
|