Cosplay/nhf/touhou/houjuu_nue/wing.py

514 lines
16 KiB
Python

"""
This file describes the shapes of the wing shells. The joints are defined in
`__init__.py`.
"""
import math
from enum import Enum
from dataclasses import dataclass
from typing import Mapping, Tuple
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 = 100
elbow_x: float = 240
elbow_y: float = 30
# Tilt of elbow w.r.t. shoulder
elbow_angle: float = 20
wrist_height: float = 70
# Bottom point of the wrist
wrist_x: float = 400
wrist_y: float = 200
# Tile of wrist w.r.t. shoulder
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
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
@property
def ring_radius(self) -> float:
dx = self.ring_x
dy = self.ring_y
return (dx * dx + dy * dy) ** 0.5
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
"""
result = (
Cq.Sketch()
.segment(
(0, 0),
(0, self.shoulder_height),
tag="shoulder")
.arc(
(0, self.shoulder_height),
(self.elbow_top_x, self.elbow_top_y),
(self.wrist_top_x, self.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")
)
result = (
result
.segment(
(self.wrist_x, self.wrist_y),
(self.arrow_x, self.arrow_y)
)
.segment(
(self.arrow_x, self.arrow_y),
(self.arrow_tip_x, self.arrow_tip_y)
)
.segment(
(self.arrow_tip_x, self.arrow_tip_y),
(self.wrist_top_x, self.wrist_top_y)
)
)
# Carve out the ring
result = result.assemble()
result = (
result
.push([(self.ring_abs_x, self.ring_abs_y)])
.circle(self.ring_radius, mode='a')
.circle(self.ring_radius_inner, mode='s')
.clean()
)
return result
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 - self.wrist_s * abscissa,
self.wrist_y - self.wrist_c * abscissa),
(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,
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:
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_wrist(), mode='i')
.reset()
.polygon(self._mask_elbow(), mode='s')
)
return profile
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 + 270),
]
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 + 270),
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
def profile_s3(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_wrist(), mode='s')
)
return profile
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 + 270),
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
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