Cosplay/nhf/touhou/houjuu_nue/wing.py

514 lines
16 KiB
Python
Raw Normal View History

"""
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-11 16:02:54 -07:00
from enum import Enum
2024-07-09 19:57:54 -07:00
from dataclasses import dataclass
2024-07-11 16:02:54 -07:00
from typing import Mapping, Tuple
2024-06-24 16:13:15 -07:00
import cadquery as Cq
from nhf import Material, Role
2024-07-04 00:42:14 -07:00
from nhf.parts.joints import HirthJoint
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
def wing_root(joint: HirthJoint,
2024-07-04 17:50:11 -07:00
bolt_diam: int = 12,
union_tol=1e-4,
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:
"""
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)
.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
2024-07-04 17:50:11 -07:00
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):
tag = f"conn_{side}{i}"
plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z")
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")
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)))
)
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
2024-07-09 21:13:16 -07:00
2024-07-11 16:02:54 -07:00
elbow_height: float = 100
elbow_x: float = 240
elbow_y: float = 30
# Tilt of elbow w.r.t. shoulder
elbow_angle: float = 20
2024-07-09 21:13:16 -07:00
wrist_height: float = 70
# Bottom point of the wrist
wrist_x: float = 400
wrist_y: float = 200
2024-07-11 16:02:54 -07:00
# Tile of wrist w.r.t. shoulder
wrist_angle: float = 40
2024-07-09 21:13:16 -07:00
# 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
2024-07-11 16:02:54 -07:00
self.elbow_theta = math.radians(self.elbow_angle)
self.elbow_c = math.cos(self.elbow_theta)
self.elbow_s = math.sin(self.elbow_theta)
self.elbow_top_x, self.elbow_top_y = self.elbow_to_abs(0, self.elbow_height)
self.wrist_theta = math.radians(self.wrist_angle)
self.wrist_c = math.cos(self.wrist_theta)
self.wrist_s = math.sin(self.wrist_theta)
self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height)
self.arrow_theta = math.radians(self.arrow_angle)
self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height)
self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \
* math.sin(self.arrow_theta - self.wrist_theta)
self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \
* math.cos(self.arrow_theta - self.wrist_theta)
# [[c, s], [-s, c]] * [ring_x, ring_y]
self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y
self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y
2024-07-09 21:13:16 -07:00
@property
def ring_radius(self) -> float:
dx = self.ring_x
dy = self.ring_y
return (dx * dx + dy * dy) ** 0.5
2024-07-11 16:02:54 -07:00
def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]:
elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s
elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c
return elbow_x, elbow_y
def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]:
wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s
wrist_y = self.wrist_y + x * self.wrist_s + y * self.wrist_c
return wrist_x, wrist_y
def profile(self) -> Cq.Sketch:
"""
Net profile of the wing starting from the wing root with no divisions
"""
2024-07-09 21:13:16 -07:00
result = (
Cq.Sketch()
.segment(
(0, 0),
(0, self.shoulder_height),
tag="shoulder")
.arc(
(0, self.shoulder_height),
2024-07-11 16:02:54 -07:00
(self.elbow_top_x, self.elbow_top_y),
(self.wrist_top_x, self.wrist_top_y),
2024-07-09 21:13:16 -07:00
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")
)
result = (
result
.segment(
(self.wrist_x, self.wrist_y),
2024-07-11 16:02:54 -07:00
(self.arrow_x, self.arrow_y)
2024-07-09 21:13:16 -07:00
)
.segment(
2024-07-11 16:02:54 -07:00
(self.arrow_x, self.arrow_y),
(self.arrow_tip_x, self.arrow_tip_y)
2024-07-09 21:13:16 -07:00
)
.segment(
2024-07-11 16:02:54 -07:00
(self.arrow_tip_x, self.arrow_tip_y),
(self.wrist_top_x, self.wrist_top_y)
2024-07-09 21:13:16 -07:00
)
)
# Carve out the ring
result = result.assemble()
result = (
result
2024-07-11 16:02:54 -07:00
.push([(self.ring_abs_x, self.ring_abs_y)])
2024-07-09 21:13:16 -07:00
.circle(self.ring_radius, mode='a')
.circle(self.ring_radius_inner, mode='s')
.clean()
)
return result
2024-07-09 19:57:54 -07:00
2024-07-11 16:02:54 -07:00
def _mask_elbow(self) -> list[Tuple[float, float]]:
"""
Polygon shape to mask out parts above the elbow
"""
abscissa = 200
return [
(0, -abscissa),
(self.elbow_x, self.elbow_y),
(self.elbow_top_x, self.elbow_top_y),
(0, abscissa)
]
def _mask_wrist(self) -> list[Tuple[float, float]]:
abscissa = 200
return [
(0, -abscissa),
(self.wrist_x, -abscissa),
(self.wrist_x, self.wrist_y),
2024-07-11 16:02:54 -07:00
(self.wrist_top_x, self.wrist_top_y),
(0, abscissa),
]
def profile_s1(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='i')
)
return profile
def surface_s1(self,
2024-07-12 11:04:28 -07:00
thickness: float = 25.4/16,
shoulder_mount_inset: float = 20,
shoulder_joint_child_height: float = 80,
elbow_mount_inset: float = 20,
elbow_joint_parent_height: float = 60,
front: bool = True) -> Cq.Workplane:
2024-07-11 16:02:54 -07:00
assert shoulder_joint_child_height < self.shoulder_height
assert elbow_joint_parent_height < self.elbow_height
h = (self.shoulder_height - shoulder_joint_child_height) / 2
tags_shoulder = [
("shoulder_bot", (shoulder_mount_inset, h), 90),
("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270),
]
h = (self.elbow_height - elbow_joint_parent_height) / 2
tags_elbow = [
("elbow_bot",
self.elbow_to_abs(-elbow_mount_inset, h),
self.elbow_angle + 90),
("elbow_top",
self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height),
self.elbow_angle + 270),
]
profile = self.profile_s1()
tags = tags_shoulder + tags_elbow
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
def profile_s2(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='s')
.reset()
.polygon(self._mask_wrist(), mode='i')
2024-07-11 16:02:54 -07:00
)
return profile
2024-07-12 11:04:28 -07:00
def surface_s2(self,
thickness: float = 25.4/16,
elbow_mount_inset: float = 20,
elbow_joint_child_height: float = 80,
wrist_mount_inset: float = 20,
wrist_joint_parent_height: float = 60,
front: bool = True) -> Cq.Workplane:
assert elbow_joint_child_height < self.elbow_height
h = (self.elbow_height - elbow_joint_child_height) / 2
tags_elbow = [
("elbow_bot",
self.elbow_to_abs(elbow_mount_inset, h),
self.elbow_angle + 90),
("elbow_top",
self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height),
self.elbow_angle - 90),
2024-07-12 11:04:28 -07:00
]
h = (self.wrist_height - wrist_joint_parent_height) / 2
tags_wrist = [
("wrist_bot",
self.wrist_to_abs(-wrist_mount_inset, h),
self.wrist_angle + 90),
("wrist_top",
self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height),
self.wrist_angle - 90),
2024-07-12 11:04:28 -07:00
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
2024-07-11 16:02:54 -07:00
def profile_s3(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_wrist(), mode='s')
)
return profile
2024-07-12 11:04:28 -07:00
def surface_s3(self,
thickness: float = 25.4/16,
wrist_mount_inset: float = 20,
wrist_joint_child_height: float = 80,
front: bool = True) -> Cq.Workplane:
assert wrist_joint_child_height < self.wrist_height
h = (self.wrist_height - wrist_joint_child_height) / 2
tags = [
("wrist_bot",
self.elbow_to_abs(wrist_mount_inset, h),
self.elbow_angle + 90),
("wrist_top",
self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height),
self.elbow_angle - 90),
2024-07-12 11:04:28 -07:00
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
2024-07-11 16:02:54 -07:00
2024-07-09 19:57:54 -07:00
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