Cosplay/nhf/touhou/houjuu_nue/wing.py

989 lines
36 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, TargetKind, target, assembly, submodel
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
from nhf.parts.joints import HirthJoint
from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint
from nhf.parts.planar import extrude_with_markers
import nhf.utils
@dataclass(kw_only=True)
class WingProfile(Model):
name: str = "wing"
base_width: float = 80.0
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
panel_thickness: float = 25.4 / 16
# 1/4" acrylic for the spacer. Anything thinner would threathen structural
# strength
spacer_thickness: float = 25.4 / 4
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
))
shoulder_angle_bias: float = 0.0
shoulder_width: float = 36.0
shoulder_tip_x: float = -260.0
shoulder_tip_y: float = 165.0
shoulder_mid_x: float = -125.0
shoulder_mid_y: float = 75.0
s1_thickness: float = 25.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=55,
),
hole_diam=6.0,
angle_neutral=15.0,
))
# Distance between the two spacers on the elbow, halved
elbow_h2: float = 5.0
s2_thickness: float = 25.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=30,
radius_disk=13.0,
radius_housing=15.0,
),
hole_pos=[10, 20],
lip_length=50,
child_arm_radius=23.0,
parent_arm_radius=30.0,
hole_diam=4.0,
angle_neutral=30.0,
))
# Distance between the two spacers on the elbow, halved
wrist_h2: float = 5.0
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
# Subclass must populate
elbow_bot_loc: Cq.Location
elbow_height: float
wrist_bot_loc: Cq.Location
wrist_height: float
elbow_rotate: float = -5.0
wrist_rotate: float = 30.0
# False for the right side, True for the left side
flip: bool = False
def __post_init__(self):
super().__init__(name=self.name)
self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height)
self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height)
self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2)
self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2)
assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness)
assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness)
self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias
self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias)
self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2
assert self.spacer_thickness == self.root_joint.child_mount_thickness
@submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model:
return self.shoulder_joint
@submodel(name="elbow-joint")
def submodel_elbow_joint(self) -> Model:
return self.elbow_joint
@submodel(name="wrist-joint")
def submodel_wrist_joint(self) -> Model:
return self.wrist_joint
@property
def root_height(self) -> float:
return self.shoulder_joint.height
@property
def shoulder_height(self) -> float:
return self.shoulder_joint.height
def outer_profile_s0(self) -> Cq.Sketch:
"""
The outer boundary of s0, used to produce the curved panel and the
top/bottom slots
"""
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
return (
Cq.Sketch()
.spline([
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y)
])
#.segment(
# (tip_x, tip_y),
# (tip_x - 10, tip_y),
#)
)
@property
def shoulder_angle_neutral(self) -> float:
"""
Returns the neutral angle of the shoulder
"""
dx = self.shoulder_mid_x - self.shoulder_tip_x
dy = -(self.shoulder_mid_y - (self.shoulder_tip_y - self.shoulder_width))
result = math.degrees(math.atan2(dy, dx))
assert result >= 0
return result
@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
sw = self.shoulder_width
sketch = (
self.outer_profile_s0()
.segment((-self.base_width, 0), (0, 0))
.segment(
(tip_x, tip_y),
(tip_x, tip_y - sw),
)
.segment(
(tip_x, tip_y - sw),
(mid_x, mid_y),
)
.segment(
(mid_x, mid_y),
(-self.base_width, 0),
)
.assemble()
.push([self.shoulder_axle_loc.to2d_pos()])
.circle(self.shoulder_joint.radius, mode='a')
.circle(self.shoulder_joint.bolt.diam_head / 2, mode='s')
)
return sketch
def outer_shell_s0(self) -> Cq.Workplane:
t = self.panel_thickness
profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals())
result = (
Cq.Workplane('XZ')
.rect(t, self.root_height + t*2, centered=(False, False))
.sweep(profile)
)
plane = result.copyWorkplane(Cq.Workplane('XZ'))
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(0, self.root_height + t*2).tagPlane("top")
return result
@submodel(name="spacer-s0-shoulder")
def spacer_s0_shoulder(self) -> MountingBox:
"""
Shoulder side serves double purpose for mounting shoulder joint and
structural support
"""
holes = [
hole
for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
Hole(x=x, y=y, tag=f"conn_top{i}"),
Hole(x=-x, y=y, tag=f"conn_bot{i}"),
]
]
return MountingBox(
length=self.shoulder_joint.height,
width=self.shoulder_joint.parent_lip_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True),
flip_y=self.flip,
)
@submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox:
"""
Base side connects to H-S joint
"""
assert self.root_joint.child_width < self.base_width
assert self.root_joint.child_corner_dx * 2 < self.base_width
assert self.root_joint.child_corner_dz * 2 < self.root_height
dy = self.root_joint.child_corner_dx
dx = self.root_joint.child_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.root_joint.child_width,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.root_joint.corner_hole_diam,
centred=(True, True),
flip_y=self.flip,
)
@submodel(name="spacer-s0-mid3")
def spacer_s0_mid3(self) -> MountingBox:
return MountingBox(
length=self.root_height,
width=40,
thickness=self.spacer_thickness,
flip_y=self.flip
)
@submodel(name="spacer-s0-mid2")
def spacer_s0_mid2(self) -> MountingBox:
return MountingBox(
length=self.root_height,
width=60,
thickness=self.spacer_thickness,
flip_y=self.flip
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10
base_dy = self.root_joint.hirth_joint.joint_height
loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
mid_spacer_loc = (
Cq.Location.from2d(0, -self.shoulder_width/2) *
self.shoulder_axle_loc *
Cq.Location.rot2d(self.shoulder_joint.angle_neutral)
)
tags = [
("shoulder",
self.shoulder_axle_loc *
self.shoulder_joint.parent_arm_loc() *
loc_tip),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
("mid3", mid_spacer_loc * Cq.Location.from2d(90, 0)),
("mid2", mid_spacer_loc * Cq.Location.from2d(150, 0)),
]
result = 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
@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,
loc=Cq.Location((0, 0, self.root_height + self.panel_thickness)))
.constrain("bot", "Fixed")
.constrain("top", "Fixed")
#.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"),
(self.spacer_s0_mid3().generate(), "mid3"),
(self.spacer_s0_mid2().generate(), "mid2"),
]:
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)
.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")
)
return result.solve()
### s1, s2, s3 ###
def profile(self) -> Cq.Sketch:
"""
Generates profile from shoulder and above. Subclass should implement
"""
def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch:
"""
Creates a cutting polygon for removing the contraction part of a joint
"""
theta = math.radians(self.elbow_joint.motion_span)
h = self.elbow_height
dx = h * math.tan(theta / 2)
dy = h
sign = -1 if self.flip else 1
points = [
(0, 0),
(dx, sign * dy),
(-dx, sign * dy),
]
return (
Cq.Sketch()
.polygon([
(loc * Cq.Location.from2d(*p)).to2d_pos()
for p in points
])
)
def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch:
"""
Creates a cutting polygon for removing the contraction part of a joint
"""
theta = math.radians(self.wrist_joint.motion_span)
dx = self.wrist_height * math.tan(theta)
dy = self.wrist_height
sign = -1 if self.flip else 1
points = [
(0, 0),
(0, -sign * dy),
(-dx, -sign * dy),
]
return (
Cq.Sketch()
.polygon([
(loc * Cq.Location.from2d(*p)).to2d_pos()
for p in points
])
)
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,
rotate: 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 = 180 if rotate else 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)
)
def _mask_elbow(self) -> list[Tuple[float, float]]:
"""
Polygon shape to mask out parts above the elbow
"""
def _mask_wrist(self) -> list[Tuple[float, float]]:
"""
Polygon shape to mask wrist
"""
def spacer_of_joint(
self,
joint: ElbowJoint,
segment_thickness: float,
dx: float) -> MountingBox:
length = joint.lip_length / 2 - dx
holes = [
Hole(x - dx)
for x in joint.hole_pos
]
mbox = MountingBox(
length=length,
width=segment_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=joint.hole_diam,
centred=(False, True),
)
return mbox
@target(name="profile-s1", kind=TargetKind.DXF)
def profile_s1(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='i')
.reset()
.push([self.elbow_axle_loc])
.each(self._elbow_joint_retract_cut_polygon, mode='s')
)
return profile
def surface_s1(self, front: bool = True) -> Cq.Workplane:
shoulder_h = self.shoulder_joint.child_height
h = (self.shoulder_joint.height - shoulder_h) / 2
tags_shoulder = [
("shoulder_bot", Cq.Location.from2d(0, h, 90)),
("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)),
]
h = self.elbow_height / 2
loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc()
tags_elbow = [
("elbow_bot", self.elbow_axle_loc * loc_elbow *
Cq.Location.from2d(0, -self.elbow_h2)),
("elbow_top", self.elbow_axle_loc * loc_elbow *
Cq.Location.from2d(0, self.elbow_h2)),
]
profile = self.profile_s1()
tags = tags_shoulder + tags_elbow
return extrude_with_markers(
profile, self.panel_thickness, tags, reverse=front)
@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,
)
@submodel(name="spacer-s1-elbow")
def spacer_s1_elbow(self) -> MountingBox:
return self.spacer_of_joint(
joint=self.elbow_joint,
segment_thickness=self.s1_thickness,
dx=self.elbow_h2,
)
@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=not is_top,
)
return result.solve()
@target(name="profile-s2", kind=TargetKind.DXF)
def profile_s2(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='s')
.reset()
.polygon(self._mask_wrist(), mode='i')
.reset()
.push([self.elbow_axle_loc])
.each(self._elbow_joint_retract_cut_polygon, mode='s')
.reset()
.push([self.wrist_axle_loc])
.each(self._wrist_joint_retract_cut_polygon, mode='s')
)
return profile
def surface_s2(self, front: bool = True) -> Cq.Workplane:
h = self.elbow_height / 2
loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip)
tags_elbow = [
("elbow_bot", self.elbow_axle_loc * loc_elbow *
Cq.Location.from2d(0, self.elbow_h2)),
("elbow_top", self.elbow_axle_loc * loc_elbow *
Cq.Location.from2d(0, -self.elbow_h2)),
]
h = self.wrist_height / 2
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc()
tags_wrist = [
("wrist_bot", self.wrist_axle_loc * loc_wrist *
Cq.Location.from2d(0, -self.wrist_h2)),
("wrist_top", self.wrist_axle_loc * loc_wrist *
Cq.Location.from2d(0, self.wrist_h2)),
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s2-elbow")
def spacer_s2_elbow(self) -> MountingBox:
return self.spacer_of_joint(
joint=self.elbow_joint,
segment_thickness=self.s2_thickness,
dx=self.elbow_h2,
)
@submodel(name="spacer-s2-wrist")
def spacer_s2_wrist(self) -> MountingBox:
return self.spacer_of_joint(
joint=self.wrist_joint,
segment_thickness=self.s2_thickness,
dx=self.wrist_h2,
)
@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,
#rotate=not is_parent,
)
return result.solve()
@target(name="profile-s3", kind=TargetKind.DXF)
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:
h = self.wrist_height / 2
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip)
tags = [
("wrist_bot", self.wrist_axle_loc * loc_wrist *
Cq.Location.from2d(0, self.wrist_h2)),
("wrist_top", self.wrist_axle_loc * loc_wrist *
Cq.Location.from2d(0, -self.wrist_h2)),
]
profile = self.profile_s3()
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s3-wrist")
def spacer_s3_wrist(self) -> MountingBox:
return self.spacer_of_joint(
joint=self.wrist_joint,
segment_thickness=self.s3_thickness,
dx=self.wrist_h2,
)
@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")
o = self.spacer_s3_wrist()
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=is_top,
)
return result.solve()
@assembly()
def assembly(self,
parts: Optional[list[str]] = None,
shoulder_deflection: float = 0.0,
elbow_wrist_deflection: float = 0.0,
root_offset: int = 5,
fastener_pos: float = 0.0,
) -> Cq.Assembly():
if parts is None:
parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
result = (
Cq.Assembly()
)
tag_top, tag_bot = "top", "bot"
if self.flip:
tag_top, tag_bot = tag_bot, tag_top
if "s0" in parts:
result.add(self.assembly_s0(), name="s0")
if "root" in parts:
result.addS(self.root_joint.assembly(
offset=root_offset,
fastener_pos=fastener_pos,
), name="root")
result.constrain("root/parent", "Fixed")
if "s0" in parts and "root" in parts:
(
result
.constrain("s0/base?conn0", "root/child?conn0", "Plane", param=0)
.constrain("s0/base?conn1", "root/child?conn1", "Plane", param=0)
.constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0)
)
if "shoulder" in parts:
angle = shoulder_deflection * self.shoulder_joint.angle_max_deflection
result.add(self.shoulder_joint.assembly(
fastener_pos=fastener_pos,
deflection=angle), name="shoulder")
if "s0" in parts and "shoulder" in parts:
(
result
.constrain(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane")
.constrain(f"s0/shoulder?conn_top1", f"shoulder/parent_{tag_top}/lip?conn1", "Plane")
.constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane")
.constrain(f"s0/shoulder?conn_bot1", f"shoulder/parent_{tag_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", f"shoulder/child/lip_{tag_top}?conn0", "Plane")
.constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane")
.constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane")
.constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane")
)
if "elbow" in parts:
angle = self.elbow_joint.motion_span * elbow_wrist_deflection
result.add(self.elbow_joint.assembly(angle=angle), name="elbow")
if "s1" in parts and "elbow" in parts:
(
result
.constrain("s1/elbow_top?conn0", f"elbow/parent_upper/lip?conn_{tag_top}0", "Plane")
.constrain("s1/elbow_top?conn1", f"elbow/parent_upper/lip?conn_{tag_top}1", "Plane")
.constrain("s1/elbow_bot?conn0", f"elbow/parent_upper/lip?conn_{tag_bot}0", "Plane")
.constrain("s1/elbow_bot?conn1", f"elbow/parent_upper/lip?conn_{tag_bot}1", "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", f"elbow/child/lip?conn_{tag_top}0", "Plane")
.constrain("s2/elbow_top?conn1", f"elbow/child/lip?conn_{tag_top}1", "Plane")
.constrain("s2/elbow_bot?conn0", f"elbow/child/lip?conn_{tag_bot}0", "Plane")
.constrain("s2/elbow_bot?conn1", f"elbow/child/lip?conn_{tag_bot}1", "Plane")
)
if "wrist" in parts:
angle = self.wrist_joint.motion_span * elbow_wrist_deflection
result.add(self.wrist_joint.assembly(angle=angle), name="wrist")
if "s2" in parts and "wrist" in parts:
# Mounted backwards to bend in other direction
(
result
.constrain("s2/wrist_top?conn0", f"wrist/parent_upper/lip?conn_{tag_bot}0", "Plane")
.constrain("s2/wrist_top?conn1", f"wrist/parent_upper/lip?conn_{tag_bot}1", "Plane")
.constrain("s2/wrist_bot?conn0", f"wrist/parent_upper/lip?conn_{tag_top}0", "Plane")
.constrain("s2/wrist_bot?conn1", f"wrist/parent_upper/lip?conn_{tag_top}1", "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", f"wrist/child/lip?conn_{tag_bot}0", "Plane")
.constrain("s3/wrist_top?conn1", f"wrist/child/lip?conn_{tag_bot}1", "Plane")
.constrain("s3/wrist_bot?conn0", f"wrist/child/lip?conn_{tag_top}0", "Plane")
.constrain("s3/wrist_bot?conn1", f"wrist/child/lip?conn_{tag_top}1", "Plane")
)
if len(parts) > 1:
result.solve()
return result
@dataclass(kw_only=True)
class WingR(WingProfile):
"""
Right side wings
"""
elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0)
elbow_height: float = 111.0
wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0)
wrist_height: float = 60.0
# 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_rel_loc: Cq.Location = Cq.Location.from2d(45.0, 25.0)
ring_radius_inner: float = 22.0
def __post_init__(self):
super().__post_init__()
assert self.arrow_angle < 0, "Arrow angle cannot be positive"
self.arrow_bot_loc = self.wrist_bot_loc \
* Cq.Location.from2d(0, -self.arrow_height)
self.arrow_other_loc = self.arrow_bot_loc \
* Cq.Location.rot2d(self.arrow_angle) \
* Cq.Location.from2d(0, self.arrow_height + self.wrist_height)
self.ring_loc = self.wrist_top_loc * self.ring_rel_loc
assert self.ring_radius > self.ring_radius_inner
@property
def ring_radius(self) -> float:
(dx, dy), _ = self.ring_rel_loc.to2d()
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_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
],
tag="s1_top")
#.segment(
# (self.wrist_x, self.wrist_y),
# (wrist_top_x, wrist_top_y),
# tag="wrist")
.spline([
(0, 0),
self.elbow_bot_loc.to2d_pos(),
self.wrist_bot_loc.to2d_pos(),
],
tag="s1_bot")
)
result = (
result
.segment(
self.wrist_bot_loc.to2d_pos(),
self.arrow_bot_loc.to2d_pos(),
)
.segment(
self.arrow_bot_loc.to2d_pos(),
self.arrow_other_loc.to2d_pos(),
)
.segment(
self.arrow_other_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
)
)
# Carve out the ring
result = result.assemble()
result = (
result
.push([self.ring_loc.to2d_pos()])
.circle(self.ring_radius, mode='a')
.circle(self.ring_radius_inner, mode='s')
.clean()
)
return result
def _mask_elbow(self) -> list[Tuple[float, float]]:
l = 200
elbow_x, _ = self.elbow_bot_loc.to2d_pos()
elbow_top_x, _ = self.elbow_top_loc.to2d_pos()
return [
(0, -l),
(elbow_x, -l),
self.elbow_bot_loc.to2d_pos(),
self.elbow_top_loc.to2d_pos(),
(elbow_top_x, l),
(0, l)
]
def _mask_wrist(self) -> list[Tuple[float, float]]:
l = 200
wrist_x, _ = self.wrist_bot_loc.to2d_pos()
_, wrist_top_y = self.wrist_top_loc.to2d_pos()
return [
(0, -l),
(wrist_x, -l),
self.wrist_bot_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
#(self.wrist_top_x, self.wrist_top_y),
(0, wrist_top_y),
]
@dataclass(kw_only=True)
class WingL(WingProfile):
elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0)
elbow_height: float = 80.0
wrist_angle: float = -45.0
wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0)
wrist_height: float = 43.0
shoulder_bezier_ext: float = 120.0
shoulder_bezier_drop: float = 15.0
elbow_bezier_ext: float = 80.0
wrist_bezier_ext: float = 30.0
arrow_length: float = 135.0
arrow_height: float = 120.0
flip: bool = True
def __post_init__(self):
assert self.wrist_height <= self.shoulder_joint.height
self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle)
self.elbow_joint.angle_neutral = 15.0
self.elbow_rotate = 5.0
self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0
self.wrist_rotate = -self.wrist_joint.angle_neutral
super().__post_init__()
def arrow_to_abs(self, x, y) -> Tuple[float, float]:
rel = Cq.Location.from2d(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2)
return (self.wrist_bot_loc * rel).to2d_pos()
def profile(self) -> Cq.Sketch:
result = (
Cq.Sketch()
.segment(
(0,0),
(0, self.shoulder_height)
)
.bezier([
(0, 0),
(self.shoulder_bezier_ext, -self.shoulder_bezier_drop),
(self.elbow_bot_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(),
self.elbow_bot_loc.to2d_pos(),
])
.bezier([
(0, self.shoulder_joint.height),
(self.shoulder_bezier_ext, self.shoulder_joint.height),
(self.elbow_top_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(),
self.elbow_top_loc.to2d_pos(),
])
.bezier([
self.elbow_bot_loc.to2d_pos(),
(self.elbow_bot_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(),
(self.wrist_bot_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(),
self.wrist_bot_loc.to2d_pos(),
])
.bezier([
self.elbow_top_loc.to2d_pos(),
(self.elbow_top_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(),
(self.wrist_top_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(),
self.wrist_top_loc.to2d_pos(),
])
)
# arrow base positions
base_u, base_v = 0.3, 0.3
result = (
result
.bezier([
self.wrist_top_loc.to2d_pos(),
(self.wrist_top_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(),
self.arrow_to_abs(base_u, base_v),
])
.bezier([
self.wrist_bot_loc.to2d_pos(),
(self.wrist_bot_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(),
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()
def _mask_elbow(self) -> list[Tuple[float, float]]:
l = 200
elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos()
elbow_top_x, _ = self.elbow_top_loc.to2d_pos()
return [
(0, -l),
(elbow_bot_x, -l),
self.elbow_bot_loc.to2d_pos(),
self.elbow_top_loc.to2d_pos(),
(elbow_top_x, l),
(0, l)
]
def _mask_wrist(self) -> list[Tuple[float, float]]:
l = 200
elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos()
_, elbow_top_y = self.elbow_top_loc.to2d_pos()
_, wrist_bot_y = self.wrist_bot_loc.to2d_pos()
return [
(0, -l),
(elbow_bot_x, wrist_bot_y),
self.wrist_bot_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
(elbow_bot_x, elbow_top_y + l),
(0, l),
]