Cosplay/nhf/touhou/houjuu_nue/wing.py

907 lines
31 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
from dataclasses import dataclass, field
from typing import Mapping, Tuple, Optional
2024-06-24 16:13:15 -07:00
import cadquery as Cq
from nhf import Material, Role
2024-07-16 15:42:39 -07:00
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
2024-07-04 00:42:14 -07:00
from nhf.parts.joints import HirthJoint
2024-07-16 14:25:17 -07:00
from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint, DiskJoint
import nhf.utils
2024-06-24 16:13:15 -07:00
2024-07-16 14:25:17 -07:00
@dataclass(kw_only=True)
class WingProfile(Model):
2024-07-09 19:57:54 -07:00
name: str = "wing"
2024-07-07 21:01:40 -07:00
2024-07-16 17:18:28 -07:00
base_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
radius=25.0,
2024-07-16 17:18:28 -07:00
radius_inner=20.0,
tooth_height=7.0,
base_height=5,
n_tooth=24,
2024-07-16 17:18:28 -07:00
))
base_width: float = 80.0
hs_joint_corner_dx: float = 17.0
hs_joint_corner_dz: float = 24.0
hs_joint_corner_hole_diam: float = 6.0
hs_joint_axis_diam: float = 12.0
base_plate_width: float = 50.0
2024-07-14 00:47:44 -07:00
panel_thickness: float = 25.4 / 16
2024-07-14 17:56:02 -07:00
spacer_thickness: float = 25.4 / 8
2024-07-16 15:42:39 -07:00
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
2024-07-16 17:18:28 -07:00
height=60.0,
2024-07-16 15:42:39 -07:00
))
shoulder_width: float = 30.0
shoulder_tip_x: float = -200.0
shoulder_tip_y: float = 160.0
shoulder_mid_x: float = -105.0
shoulder_mid_y: float = 75.0
2024-07-09 19:57:54 -07:00
s1_thickness: float = 25.0
2024-07-09 21:13:16 -07:00
2024-07-16 14:25:17 -07:00
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=55,
),
flip=False,
))
2024-07-09 21:13:16 -07:00
s2_thickness: float = 25.0
2024-07-16 14:25:17 -07:00
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=45,
),
flip=True,
))
2024-07-09 21:13:16 -07:00
s3_thickness: float = 25.0
mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT
mat_bracket: Material = Material.ACRYLIC_TRANSPARENT
mat_hs_joint: Material = Material.PLASTIC_PLA
role_panel: Role = Role.STRUCTURE
2024-07-16 17:18:28 -07:00
# Subclass must populate
elbow_x: float
elbow_y: float
elbow_angle: float
elbow_height: float
wrist_x: float
wrist_y: float
wrist_angle: float
wrist_height: float
flip: bool = False
2024-07-09 21:13:16 -07:00
def __post_init__(self):
super().__init__(name=self.name)
2024-07-09 21:13:16 -07:00
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)
@property
def root_height(self) -> float:
return self.shoulder_joint.height
2024-07-16 17:18:28 -07:00
@property
def shoulder_height(self) -> float:
return self.shoulder_joint.height
@target(name="base-hs-joint")
def base_hs_joint(self) -> Cq.Workplane:
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
coupling, a cylindrical base, and a mounting base.
"""
hirth = self.base_joint.generate(is_mated=True)
dy = self.hs_joint_corner_dx
dx = self.hs_joint_corner_dz
conn = [
(-dx, -dy),
(dx, -dy),
(dx, dy),
(-dx, dy),
]
result = (
Cq.Workplane('XY')
.box(
self.root_height,
self.base_plate_width,
self.base_joint.base_height,
centered=(True, True, False))
#.translate((0, 0, -self.base_joint.base_height))
#.edges("|Z")
#.fillet(self.hs_joint_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.hole(self.hs_joint_corner_hole_diam)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"conn{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(self.hs_joint_axis_diam)
)
return result
2024-07-16 15:42:39 -07:00
@target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self) -> Cq.Sketch:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
mid_x = self.shoulder_mid_x
mid_y = self.shoulder_mid_y
sketch = (
Cq.Sketch()
.segment((-self.base_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),
(mid_x, mid_y),
)
.segment(
(mid_x, mid_y),
(-self.base_width, 0),
)
.assemble()
)
return sketch
def outer_shell_s0(self) -> Cq.Workplane:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
t = self.panel_thickness
edge = Cq.Edge.makeSpline([
Cq.Vector(x, y, 0)
for x,y in [
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y)
]
])
result = (
Cq.Workplane('XZ')
.rect(t, self.root_height + t*2, centered=(False, False))
.sweep(edge)
)
plane = result.copyWorkplane(Cq.Workplane('XZ'))
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(0, self.root_height + t*2).tagPlane("top")
return result
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s0-shoulder")
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),
2024-07-16 17:18:28 -07:00
flip_y=self.flip,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox:
"""
Should be cut
"""
assert self.base_plate_width < self.base_width
assert self.hs_joint_corner_dx * 2 < self.base_width
assert self.hs_joint_corner_dz * 2 < self.root_height
dy = self.hs_joint_corner_dx
dx = self.hs_joint_corner_dz
holes = [
Hole(x=-dx, y=-dy),
Hole(x=dx, y=-dy),
Hole(x=dx, y=dy),
Hole(x=-dx, y=dy),
]
return MountingBox(
length=self.root_height,
width=self.base_plate_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.hs_joint_corner_hole_diam,
centred=(True, True),
2024-07-16 17:18:28 -07:00
flip_y=self.flip,
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.base_plate_width) / 2
base_dy = self.base_joint.joint_height
tags = [
("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0),
("base", (base_dx, base_dy), 90),
]
result = nhf.utils.extrude_with_markers(
self.profile_s0(),
self.panel_thickness,
tags,
reverse=not top,
)
h = self.panel_thickness if top else 0
result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner")
return result
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s0(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s0(top=True), name="bot",
material=self.mat_panel, role=self.role_panel)
.addS(self.surface_s0(top=False), name="top",
material=self.mat_panel, role=self.role_panel)
.constrain("bot@faces@>Z", "top@faces@<Z", "Point",
param=self.shoulder_joint.height)
.addS(self.outer_shell_s0(), name="outer_shell",
material=self.mat_panel, role=self.role_panel)
.constrain("bot?corner", "outer_shell?bot", "Plane", param=0)
.constrain("top?corner", "outer_shell?top", "Plane", param=0)
)
for o, tag in [
(self.spacer_s0_shoulder().generate(), "shoulder"),
(self.spacer_s0_base().generate(), "base")
]:
2024-07-16 17:18:28 -07:00
top_tag, bot_tag = "top", "bot"
if self.flip:
top_tag, bot_tag = bot_tag, top_tag
(
result
.addS(o, name=tag,
role=Role.STRUCTURE | Role.CONNECTION,
material=self.mat_bracket)
2024-07-16 17:18:28 -07:00
.constrain(f"{tag}?{bot_tag}", f"bot?{tag}", "Plane")
.constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane")
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
)
hs_joint = self.base_hs_joint()
(
result
.addS(hs_joint, name="hs", role=Role.CHILD, material=self.mat_hs_joint)
.constrain("hs?conn0", "base?conn0", "Plane", param=0)
.constrain("hs?conn1", "base?conn1", "Plane", param=0)
.constrain("hs?conn2", "base?conn2", "Plane", param=0)
)
return result.solve()
### s1, s2, s3 ###
2024-07-16 17:18:28 -07:00
def profile(self) -> Cq.Sketch:
"""
Generates profile from shoulder and above
"""
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
.addS(
spacer,
name=point_tag,
material=self.mat_bracket,
role=self.role_panel)
.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)
)
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
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
"""
2024-07-16 17:18:28 -07:00
l = 200
2024-07-11 16:02:54 -07:00
return [
2024-07-16 17:18:28 -07:00
(0, -l),
(self.elbow_x, -l),
2024-07-11 16:02:54 -07:00
(self.elbow_x, self.elbow_y),
(self.elbow_top_x, self.elbow_top_y),
2024-07-16 17:18:28 -07:00
(self.elbow_top_x, l),
(0, l)
2024-07-11 16:02:54 -07:00
]
def _mask_wrist(self) -> list[Tuple[float, float]]:
2024-07-16 17:18:28 -07:00
l = 200
2024-07-11 16:02:54 -07:00
return [
2024-07-16 17:18:28 -07:00
(0, -l),
(self.wrist_x, -l),
(self.wrist_x, self.wrist_y),
2024-07-11 16:02:54 -07:00
(self.wrist_top_x, self.wrist_top_y),
2024-07-16 17:18:28 -07:00
(self.wrist_top_x, l),
(0, l),
2024-07-11 16:02:54 -07:00
]
2024-07-16 15:42:39 -07:00
@target(name="profile-s1", kind=TargetKind.DXF)
2024-07-11 16:02:54 -07:00
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,
2024-07-12 11:04:28 -07:00
front: bool = True) -> Cq.Workplane:
shoulder_h = self.shoulder_joint.child_height
h = (self.shoulder_joint.height - shoulder_h) / 2
2024-07-11 16:02:54 -07:00
tags_shoulder = [
("shoulder_bot", (shoulder_mount_inset, h), 90),
("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270),
2024-07-11 16:02:54 -07:00
]
elbow_h = self.elbow_joint.parent_beam.total_height
h = (self.elbow_height - elbow_h) / 2
2024-07-11 16:02:54 -07:00
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),
2024-07-11 16:02:54 -07:00
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)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s1-shoulder")
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,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s1-elbow")
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,
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s1(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s1(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s1(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.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()
2024-07-11 16:02:54 -07:00
2024-07-16 15:42:39 -07:00
@target(name="profile-s2", kind=TargetKind.DXF)
2024-07-11 16:02:54 -07:00
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 = 0,
wrist_mount_inset: float = 0,
2024-07-12 11:04:28 -07:00
front: bool = True) -> Cq.Workplane:
elbow_h = self.elbow_joint.child_beam.total_height
h = (self.elbow_height - elbow_h) / 2
2024-07-12 11:04:28 -07:00
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),
2024-07-12 11:04:28 -07:00
]
wrist_h = self.wrist_joint.parent_beam.total_height
h = (self.wrist_height - wrist_h) / 2
2024-07-12 11:04:28 -07:00
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),
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-16 15:42:39 -07:00
@submodel(name="spacer-s2-elbow")
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,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s2-wrist")
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,
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s2(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s2(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s2(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.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()
2024-07-12 11:04:28 -07:00
2024-07-16 15:42:39 -07:00
@target(name="profile-s3", kind=TargetKind.DXF)
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,
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
2024-07-12 11:04:28 -07:00
tags = [
("wrist_bot",
self.wrist_to_abs(wrist_mount_inset, h),
self.wrist_angle + 90),
2024-07-12 11:04:28 -07:00
("wrist_top",
self.wrist_to_abs(wrist_mount_inset, h + wrist_h),
self.wrist_angle - 90),
2024-07-12 11:04:28 -07:00
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s3-wrist")
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
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s3(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s3(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s3(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.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()
2024-07-11 16:02:54 -07:00
2024-07-16 15:42:39 -07:00
@assembly()
def assembly(self,
parts: Optional[list[str]] = None,
angle_elbow_wrist: float = 0.0,
) -> Cq.Assembly():
2024-07-16 14:25:17 -07:00
assert not self.elbow_joint.flip
assert self.wrist_joint.flip
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(angle=angle_elbow_wrist), 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(angle=angle_elbow_wrist), name="wrist")
if "s2" in parts and "wrist" in parts:
# Mounted backwards to bend in other direction
(
result
2024-07-16 14:25:17 -07:00
.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
2024-07-16 14:25:17 -07:00
.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")
)
2024-07-16 12:03:51 -07:00
if len(parts) > 1:
result.solve()
2024-07-09 19:57:54 -07:00
2024-07-16 12:03:51 -07:00
return result
2024-07-16 17:18:28 -07:00
@dataclass(kw_only=True)
class WingR(WingProfile):
"""
Right side wings
"""
elbow_height: float = 111.0
elbow_x: float = 363.0
elbow_y: float = 44.0
# Tilt of elbow w.r.t. shoulder
elbow_angle: float = 30.0
wrist_height: float = 60.0
# Bottom point of the wrist
wrist_x: float = 403.0
wrist_y: float = 253.0
# 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 = 8
# Relative (in wrist coordinate) centre of the ring
ring_x: float = 45
ring_y: float = 25
ring_radius_inner: float = 22
def __post_init__(self):
super().__post_init__()
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
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 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")
.spline([
(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")
.spline([
(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
@dataclass(kw_only=True)
class WingL(WingProfile):
elbow_x: float = 230.0
elbow_y: float = 110.0
elbow_angle: float = -10.0
elbow_height: float = 80.0
wrist_x: float = 480.0
wrist_y: float = 0.0
wrist_angle: float = -45
wrist_height: float = 43.0
shoulder_bezier_ext: float = 80.0
elbow_bezier_ext: float = 100.0
wrist_bezier_ext: float = 30.0
arrow_length: float = 135.0
arrow_height: float = 120.0
flip: bool = True
def __post_init__(self):
super().__post_init__()
assert self.wrist_height <= self.shoulder_joint.height
def arrow_to_abs(self, x, y) -> Tuple[float, float]:
return self.wrist_to_abs(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2)
def profile(self) -> Cq.Sketch:
result = (
Cq.Sketch()
.segment(
(0,0),
(0, self.shoulder_height)
)
#.spline([
# (0, 0),
# self.elbow_to_abs(0, 0),
# self.wrist_to_abs(0, 0),
#])
#.spline([
# (0, self.shoulder_height),
# self.elbow_to_abs(0, self.elbow_height),
# self.wrist_to_abs(0, self.wrist_height),
#])
.bezier([
(0, 0),
(self.shoulder_bezier_ext, 0),
self.elbow_to_abs(-self.elbow_bezier_ext, 0),
self.elbow_to_abs(0, 0),
])
.bezier([
(0, self.shoulder_joint.height),
(self.shoulder_bezier_ext, self.shoulder_joint.height),
self.elbow_to_abs(-self.elbow_bezier_ext, self.elbow_height),
self.elbow_to_abs(0, self.elbow_height),
])
.bezier([
self.elbow_to_abs(0, 0),
self.elbow_to_abs(self.elbow_bezier_ext, 0),
self.wrist_to_abs(-self.wrist_bezier_ext, 0),
self.wrist_to_abs(0, 0),
])
.bezier([
self.elbow_to_abs(0, self.elbow_height),
self.elbow_to_abs(self.elbow_bezier_ext, self.elbow_height),
self.wrist_to_abs(-self.wrist_bezier_ext, self.wrist_height),
self.wrist_to_abs(0, self.wrist_height),
])
)
# arrow base positions
base_u, base_v = 0.3, 0.3
result = (
result
.bezier([
self.wrist_to_abs(0, self.wrist_height),
self.wrist_to_abs(self.wrist_bezier_ext, self.wrist_height),
self.arrow_to_abs(base_u, base_v),
])
.bezier([
self.wrist_to_abs(0, 0),
self.wrist_to_abs(self.wrist_bezier_ext, 0),
self.arrow_to_abs(base_u, -base_v),
])
)
# Create the arrow
arrow_beziers = [
[
(0, 1),
(0.3, 1),
(0.8, .2),
(1, 0),
],
[
(0, 1),
(0.1, 0.8),
(base_u, base_v),
]
]
arrow_beziers = [
l2
for l in arrow_beziers
for l2 in [l, [(x, -y) for x,y in l]]
]
for line in arrow_beziers:
result = result.bezier([self.arrow_to_abs(x, y) for x,y in line])
return result.assemble()