948 lines
33 KiB
Python
948 lines
33 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 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_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
|
|
radius=25.0,
|
|
radius_inner=15.0,
|
|
tooth_height=7.0,
|
|
base_height=5,
|
|
n_tooth=24,
|
|
))
|
|
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
|
|
|
|
panel_thickness: float = 25.4 / 16
|
|
spacer_thickness: float = 25.4 / 8
|
|
|
|
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
|
|
))
|
|
shoulder_width: float = 36.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
|
|
|
|
s1_thickness: float = 25.0
|
|
|
|
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
|
|
disk_joint=DiskJoint(
|
|
movement_angle=55,
|
|
),
|
|
hole_diam=6.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=45,
|
|
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,
|
|
))
|
|
# 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
|
|
|
|
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.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral
|
|
|
|
@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
|
|
|
|
@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
|
|
|
|
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()
|
|
)
|
|
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:
|
|
"""
|
|
Should be cut
|
|
"""
|
|
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:
|
|
"""
|
|
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),
|
|
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
|
|
sw = self.shoulder_width
|
|
|
|
axle_dist = self.shoulder_joint.parent_lip_ext
|
|
theta = math.radians(self.shoulder_joint.angle_neutral)
|
|
c, s = math.cos(-theta), math.sin(-theta)
|
|
tags = [
|
|
# transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2)
|
|
("shoulder", Cq.Location.from2d(
|
|
self.shoulder_tip_x + axle_dist * c + (-sw/2) * s,
|
|
self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c,
|
|
self.shoulder_joint.angle_neutral)),
|
|
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
|
|
]
|
|
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")
|
|
]:
|
|
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")
|
|
)
|
|
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 ###
|
|
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,
|
|
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,
|
|
bot=False) -> 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')
|
|
)
|
|
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
|
|
tags_elbow = [
|
|
("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d(
|
|
-self.elbow_joint.parent_arm_radius,
|
|
h - self.elbow_h2)),
|
|
("elbow_top", self.elbow_bot_loc * Cq.Location.from2d(
|
|
-self.elbow_joint.parent_arm_radius,
|
|
h + 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')
|
|
)
|
|
return profile
|
|
def surface_s2(self, front: bool = True) -> Cq.Workplane:
|
|
h = self.elbow_height / 2
|
|
tags_elbow = [
|
|
("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d(
|
|
self.elbow_joint.child_arm_radius,
|
|
h - self.elbow_h2,
|
|
180)),
|
|
("elbow_top", self.elbow_bot_loc * Cq.Location.from2d(
|
|
self.elbow_joint.child_arm_radius,
|
|
h + self.elbow_h2,
|
|
180)),
|
|
]
|
|
h = self.wrist_height / 2
|
|
tags_wrist = [
|
|
("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d(
|
|
-self.wrist_joint.parent_arm_radius,
|
|
h - self.wrist_h2)),
|
|
("wrist_top", self.wrist_bot_loc * Cq.Location.from2d(
|
|
-self.wrist_joint.parent_arm_radius,
|
|
h + 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=True,
|
|
)
|
|
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
|
|
tags = [
|
|
("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d(
|
|
self.wrist_joint.child_arm_radius,
|
|
h - self.wrist_h2,
|
|
180)),
|
|
("wrist_top", self.wrist_bot_loc * Cq.Location.from2d(
|
|
self.wrist_joint.child_arm_radius,
|
|
h + self.wrist_h2,
|
|
180)),
|
|
]
|
|
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,
|
|
angle_elbow_wrist: float = 0.0,
|
|
) -> Cq.Assembly():
|
|
if parts is None:
|
|
parts = ["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 "shoulder" in parts:
|
|
result.add(self.shoulder_joint.assembly(), 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:
|
|
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/lip?conn_top0", "Plane")
|
|
.constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane")
|
|
.constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane")
|
|
.constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "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/lip?conn_top0", "Plane")
|
|
.constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane")
|
|
.constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane")
|
|
.constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "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
|
|
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/lip?conn_bot0", "Plane")
|
|
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/lip?conn_bot1", "Plane")
|
|
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/lip?conn_top0", "Plane")
|
|
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/lip?conn_top1", "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/lip?conn_bot0", "Plane")
|
|
.constrain("s3/wrist_top?conn1", "wrist/child/lip?conn_bot1", "Plane")
|
|
.constrain("s3/wrist_bot?conn0", "wrist/child/lip?conn_top0", "Plane")
|
|
.constrain("s3/wrist_bot?conn1", "wrist/child/lip?conn_top1", "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(285.0, 5.0, 25.0)
|
|
elbow_height: float = 111.0
|
|
|
|
wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 253.0, 40.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(230.0, 110.0, -10.0)
|
|
elbow_height: float = 80.0
|
|
|
|
wrist_angle: float = -45.0
|
|
wrist_bot_loc: Cq.Location = Cq.Location.from2d(480.0, 0.0, -45.0)
|
|
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):
|
|
assert self.wrist_height <= self.shoulder_joint.height
|
|
self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle)
|
|
|
|
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, 0),
|
|
(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),
|
|
]
|