Cosplay/nhf/touhou/houjuu_nue/wing.py

614 lines
22 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, field
from typing import Mapping, Tuple, Optional
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
from nhf.parts.joints import HirthJoint
from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint
import nhf.utils
@dataclass
class WingProfile(Model):
name: str = "wing"
base_joint: HirthJoint = field(default_factory=lambda: HirthJoint())
root_width: float = 80.0
hs_joint_corner_dx: float = 30.0
hs_joint_corner_hole_diam: float = 6.0
panel_thickness: float = 25.4 / 16
spacer_thickness: float = 25.4 / 8
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint())
shoulder_width: float = 30.0
shoulder_tip_x: float = -200.0
shoulder_tip_y: float = 160.0
s1_thickness: float = 25.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
elbow_height: float = 100
elbow_x: float = 240
elbow_y: float = 30
# Tilt of elbow w.r.t. shoulder
elbow_angle: float = 20
s2_thickness: float = 25.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint())
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
s3_thickness: float = 25.0
# 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
material_panel: Material = Material.ACRYLIC_TRANSPARENT
material_bracket: Material = Material.ACRYLIC_TRANSPARENT
def __post_init__(self):
super().__init__(name=self.name)
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 root_height(self) -> float:
return self.shoulder_joint.height
def profile_s0(self) -> Cq.Sketch:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
sketch = (
Cq.Sketch()
.segment((-self.root_width, 0), (0, 0))
.spline([
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y)
])
.segment(
(tip_x, tip_y),
(tip_x, tip_y - self.shoulder_width)
)
.segment(
(tip_x, tip_y - self.shoulder_width),
(-self.root_width, 0)
)
.assemble()
)
return sketch
def spacer_s0_shoulder(self) -> MountingBox:
"""
Should be cut
"""
holes = [
hole
for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
Hole(x=x, tag=f"conn_top{i}"),
Hole(x=-x, tag=f"conn_bot{i}"),
]
]
return MountingBox(
length=self.shoulder_joint.height,
width=self.shoulder_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True),
)
def spacer_s0_base(self) -> MountingBox:
"""
Should be cut
"""
dx = self.hs_joint_corner_dx
holes = [
Hole(x=-dx, y=-dx),
Hole(x=dx, y=-dx),
Hole(x=dx, y=dx),
Hole(x=-dx, y=dx),
]
return MountingBox(
length=self.root_height,
width=self.root_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.hs_joint_corner_hole_diam,
centred=(True, True),
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
tags = [
("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0),
("base", (0, 0), 90),
]
return nhf.utils.extrude_with_markers(
self.profile_s0(),
self.panel_thickness,
tags,
reverse=not top,
)
def assembly_s0(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s0(top=True), name="bot", color=self.material_panel.color)
.add(self.surface_s0(top=False), name="top", color=self.material_panel.color)
.constrain("bot@faces@>Z", "top@faces@<Z", "Point",
param=self.shoulder_joint.height)
)
for o, tag in [
(self.spacer_s0_shoulder().generate(), "shoulder"),
(self.spacer_s0_base().generate(), "base")
]:
(
result
.add(o, name=tag, color=self.material_bracket.color)
.constrain(f"{tag}?bot", f"bot?{tag}", "Plane")
.constrain(f"{tag}?top", f"top?{tag}", "Plane")
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
)
hirth = self.base_joint.generate()
(
result
.add(hirth, name="hs", color=Role.CHILD.color)
.constrain("hs@faces@<Z", "base?dir", "Plane")
)
return result.solve()
### s1, s2, s3 ###
def _assembly_insert_spacer(
self,
a: Cq.Assembly,
spacer: Cq.Workplane,
point_tag: str,
front_tag: str = "front",
back_tag: str = "back",
flipped: bool = False,
):
"""
For a child joint facing up, front panel should be on the right, back
panel on the left
"""
site_front, site_back = "right", "left"
if flipped:
site_front, site_back = site_back, site_front
angle = 0
(
a
.add(spacer,
name=point_tag,
color=self.material_bracket.color)
.constrain(f"{front_tag}?{point_tag}",
f"{point_tag}?{site_front}", "Plane")
.constrain(f"{back_tag}?{point_tag}",
f"{point_tag}?{site_back}", "Plane")
.constrain(f"{point_tag}?dir", f"{front_tag}?{point_tag}_dir",
"Axis", param=angle)
)
@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_joint.height),
tag="shoulder")
.arc(
(0, self.shoulder_joint.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, -abscissa),
(self.wrist_x, self.wrist_y),
(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,
shoulder_mount_inset: float = 0,
elbow_mount_inset: float = 0,
front: bool = True) -> Cq.Workplane:
shoulder_h = self.shoulder_joint.child_height
h = (self.shoulder_joint.height - shoulder_h) / 2
tags_shoulder = [
("shoulder_bot", (shoulder_mount_inset, h), 90),
("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270),
]
elbow_h = self.elbow_joint.parent_beam.total_height
h = (self.elbow_height - elbow_h) / 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_h),
self.elbow_angle + 270),
]
profile = self.profile_s1()
tags = tags_shoulder + tags_elbow
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
def spacer_s1_shoulder(self) -> MountingBox:
holes = [
Hole(x)
for x in self.shoulder_joint.child_conn_hole_pos
]
return MountingBox(
length=50.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.child_conn_hole_diam,
)
def spacer_s1_elbow(self) -> MountingBox:
holes = [
Hole(x)
for x in self.elbow_joint.parent_hole_pos()
]
return MountingBox(
length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
def assembly_s1(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s1(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s1(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("shoulder")
o = self.spacer_s1_shoulder().generate() if is_parent else self.spacer_s1_elbow().generate()
self._assembly_insert_spacer(
result,
o,
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def profile_s2(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='s')
.reset()
.polygon(self._mask_wrist(), mode='i')
)
return profile
def surface_s2(self,
thickness: float = 25.4/16,
elbow_mount_inset: float = 0,
wrist_mount_inset: float = 0,
front: bool = True) -> Cq.Workplane:
elbow_h = self.elbow_joint.child_beam.total_height
h = (self.elbow_height - elbow_h) / 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_h),
self.elbow_angle - 90),
]
wrist_h = self.wrist_joint.parent_beam.total_height
h = (self.wrist_height - wrist_h) / 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_h),
self.wrist_angle - 90),
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
def spacer_s2_elbow(self) -> MountingBox:
holes = [
Hole(x)
for x in self.elbow_joint.child_hole_pos()
]
return MountingBox(
length=50.0, # FIXME: magic
width=self.s2_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
def spacer_s2_wrist(self) -> MountingBox:
holes = [
Hole(x)
for x in self.wrist_joint.parent_hole_pos()
]
return MountingBox(
length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.wrist_joint.hole_diam,
)
def assembly_s2(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s2(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s2(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("elbow")
o = self.spacer_s2_elbow() if is_parent else self.spacer_s2_wrist()
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def profile_s3(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_wrist(), mode='s')
)
return profile
def surface_s3(self,
front: bool = True) -> Cq.Workplane:
wrist_mount_inset = 0
wrist_h = self.wrist_joint.child_beam.total_height
h = (self.wrist_height - wrist_h) / 2
tags = [
("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_h),
self.wrist_angle - 90),
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
def spacer_s3_wrist(self) -> MountingBox:
holes = [
Hole(x)
for x in self.wrist_joint.child_hole_pos()
]
return MountingBox(
length=70.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.wrist_joint.hole_diam
)
def assembly_s3(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.surface_s3(front=True), name="front",
color=self.material_panel.color)
.constrain("front", "Fixed")
.add(self.surface_s3(front=False), name="back",
color=self.material_panel.color)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["wrist_bot", "wrist_top"]:
is_top = t.endswith("_top")
is_parent = True
o = self.spacer_s3_wrist()
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=is_top != is_parent,
)
return result.solve()
def assembly(self,
parts: Optional[list[str]] = None
) -> Cq.Assembly():
if parts is None:
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
result = (
Cq.Assembly()
)
if "s0" in parts:
result.add(self.assembly_s0(), name="s0")
if "shoulder" in parts:
result.add(self.shoulder_joint.assembly(), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result
.constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane")
.constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane")
.constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane")
.constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane")
)
if "s1" in parts:
result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts:
(
result
.constrain("s1/shoulder_top?conn0", "shoulder/child/lip_top?conn0", "Plane")
.constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane")
.constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane")
.constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane")
)
if "elbow" in parts:
result.add(self.elbow_joint.assembly(), name="elbow")
if "s1" in parts and "elbow" in parts:
(
result
.constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane")
.constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane")
.constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane")
.constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane")
)
if "s2" in parts:
result.add(self.assembly_s2(), name="s2")
if "s2" in parts and "elbow" in parts:
(
result
.constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane")
.constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane")
.constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane")
.constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane")
)
if "wrist" in parts:
result.add(self.wrist_joint.assembly(), name="wrist")
if "s2" in parts and "wrist" in parts:
(
result
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane")
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane")
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane")
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane")
)
if "s3" in parts:
result.add(self.assembly_s3(), name="s3")
if "s3" in parts and "wrist" in parts:
(
result
.constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane")
.constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane")
.constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane")
.constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane")
)
return result.solve()