Cosplay/nhf/touhou/houjuu_nue/wing.py

1673 lines
60 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.parts.planar import extrude_with_markers
from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint
from nhf.touhou.houjuu_nue.electronics import (
LINEAR_ACTUATOR_10,
LINEAR_ACTUATOR_21,
LINEAR_ACTUATOR_50,
ElectronicBoard,
ElectronicBoardBattery,
LightStrip,
ElectronicBoard,
ELECTRONIC_MOUNT_HEXNUT,
)
import nhf.utils
ELBOW_PARAMS = dict(
hole_diam=4.0,
actuator=LINEAR_ACTUATOR_50,
parent_arm_width=15,
)
ELBOW_DISK_PARAMS = dict(
housing_thickness=2.5,
disk_thickness=6.8,
tongue_thickness=12.3,
)
WRIST_DISK_PARAMS = dict(
movement_angle=30,
radius_disk=13.0,
radius_housing=15.0,
)
WRIST_PARAMS = dict(
)
@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
# s0 is armoured
panel_thickness_s0: float = 25.4 / 8
# 1/4" acrylic for the spacer. Anything thinner would threathen structural
# strength
spacer_thickness: float = 25.4 / 4
rod_width: float = 10.0
panel_s0_inner_trunc = 0.05
light_strip: LightStrip = LightStrip()
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_tip_bezier_x: float = 100.0
shoulder_tip_bezier_y: float = -50.0
shoulder_base_bezier_x: float = -30.0
shoulder_base_bezier_y: float = 30.0
s0_hole_width: float = 40.0
s0_hole_height: float = 10.0
s0_top_hole: bool = False
s0_bot_hole: bool = True
electronic_board: ElectronicBoard = field(default_factory=lambda: ElectronicBoardBattery())
s1_thickness: float = 25.0
elbow_joint: ElbowJoint
# Distance between the two spacers on the elbow, halved
elbow_h2: float = 5.0
wrist_joint: ElbowJoint
# Distance between the two spacers on the elbow, halved
wrist_h2: float = 5.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
wrist_rotate: float = -30.0
# Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side)
elbow_axle_pos: float
wrist_axle_pos: float
elbow_joint_overlap_median: float
wrist_joint_overlap_median: float
# False for the right side, True for the left side
flip: bool
def __post_init__(self):
super().__init__(name=self.name)
assert self.electronic_board.length == self.shoulder_height
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 * self.elbow_axle_pos)
self.wrist_axle_loc = self.wrist_bot_loc * \
Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos)
#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, 0)
self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2
assert self.spacer_thickness == self.root_joint.child_mount_thickness
@property
def s2_thickness(self) -> float:
"""
s2 needs to duck under s1, so its thinner
"""
return self.s1_thickness - 2 * self.panel_thickness
@property
def s3_thickness(self) -> float:
"""
s3 does not need to duck under s2
"""
extra = 2 * self.panel_thickness if self.flip else 0
return self.s1_thickness - 2 * self.panel_thickness - extra
@submodel(name="root-joint")
def submodel_root_joint(self) -> Model:
return self.root_joint
@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
@submodel(name="electronic-board")
def submodel_electronic_board(self) -> Model:
return self.electronic_board
@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.Edge:
"""
The outer boundary of s0 top/bottom slots
"""
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
return Cq.Edge.makeSpline(
[
Cq.Vector(*p)
for p in [
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y),
]
]
)
def inner_profile_s0(self, trunc: float=0.0) -> Cq.Edge:
"""
The inner boundary of s0
"""
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
dx2 = self.shoulder_tip_bezier_x
dy2 = self.shoulder_tip_bezier_y
dx1 = self.shoulder_base_bezier_x
dy1 = self.shoulder_base_bezier_y
sw = self.shoulder_width
points = [
(tip_x, tip_y - sw),
(tip_x + dx2, tip_y - sw + dy2),
(-self.base_width + dx1, dy1),
(-self.base_width, 0),
]
bezier = Cq.Edge.makeBezier(
[Cq.Vector(x, y) for x, y in points]
)
if trunc == 0.0:
return bezier
tip = bezier.positionAt(d=trunc, mode='parameter')
tangent = bezier.tangentAt(locationParam=trunc, mode='parameter')
points = [
tip,
tip + tangent,
Cq.Vector(-self.base_width + dx1, dy1),
Cq.Vector(-self.base_width, 0),
]
return Cq.Edge.makeBezier(points)
@property
def shoulder_angle_neutral(self) -> float:
"""
Returns the neutral angle of the shoulder
"""
result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x))
assert result >= 0
return result / 2
@target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self, top: bool = True) -> Cq.Sketch:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
sw = self.shoulder_width
sketch = (
Cq.Sketch()
.edge(self.outer_profile_s0())
.segment((-self.base_width, 0), (0, 0))
.segment(
(tip_x, tip_y),
(tip_x, tip_y - sw),
)
.edge(self.inner_profile_s0())
.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')
)
top = top == self.flip
if (self.s0_top_hole and top) or (self.s0_bot_hole and not top):
assert self.base_width > self.s0_hole_width
x = (self.base_width - self.s0_hole_width) / 2
sketch = (
sketch
.reset()
.polygon([
(-x, 0),
(-x, self.s0_hole_height),
(-self.base_width + x, self.s0_hole_height),
(-self.base_width + x, 0),
], mode='s')
)
return sketch
def outer_shell_s0(self) -> Cq.Workplane:
t = self.panel_thickness_s0
profile = self.outer_profile_s0()
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
def inner_shell_s0(self) -> Cq.Workplane:
t = self.panel_thickness_s0
profile = self.inner_profile_s0(trunc=self.panel_s0_inner_trunc)
result = (
Cq.Workplane('XZ')
.moveTo(-t, 0)
.rect(t, self.root_height + t*2, centered=(False, False))
.sweep(profile, normal=(0,-1,0))
)
plane = result.copyWorkplane(Cq.Workplane('XZ'))
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(0, self.root_height + t*2).tagPlane("top")
return result
@target(name="profile-s0-outer-shell", kind=TargetKind.DXF)
def outer_shell_s0_profile(self) -> Cq.Sketch:
"""
This part should be laser cut and then bent on a falsework to create the required shape.
"""
length = self.outer_profile_s0().Length()
height = self.root_height + self.panel_thickness_s0 * 2
return Cq.Sketch().rect(length, height)
@target(name="profile-s0-inner-shell", kind=TargetKind.DXF)
def inner_shell_s0_profile(self) -> Cq.Sketch:
"""
This part should be laser cut and then bent on a falsework to create the required shape.
"""
length = self.inner_profile_s0(trunc=self.panel_s0_inner_trunc).Length()
height = self.root_height + self.panel_thickness_s0 * 2
return Cq.Sketch().rect(length, height)
@submodel(name="spacer-s0-shoulder-inner")
def spacer_s0_shoulder(self, left: bool=True) -> MountingBox:
"""
Shoulder side serves double purpose for mounting shoulder joint and
structural support
"""
sign = 1 if left else -1
holes = [
hole
for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
Hole(x=x, y=sign * y, tag=f"conn_top{i}"),
Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"),
]
]
def post(sketch: Cq.Sketch) -> Cq.Sketch:
"""
Carve out the middle if this is closer to the front
"""
if left:
return sketch
return (
sketch
.push([(0,0)])
.rect(
w=self.shoulder_joint.parent_lip_gap,
h=self.shoulder_joint.parent_lip_width,
mode='s'
)
)
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,
centre_bot_top_tags=True,
profile_callback=post,
)
@submodel(name="spacer-s0-shoulder-outer")
def spacer_s0_shoulder_outer(self) -> MountingBox:
return self.spacer_s0_shoulder(left=False)
@submodel(name="spacer-s0-base")
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),
Hole(x=0, y=0, diam=self.root_joint.axis_diam, tag="axle"),
]
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-electronic")
def spacer_s0_electronic_mount(self) -> MountingBox:
"""
This one has hexagonal holes
"""
face = ELECTRONIC_MOUNT_HEXNUT.cutting_face()
holes = [
Hole(x=h.x, y=h.y, face=face, tag=h.tag)
for h in self.electronic_board.mount_holes
]
return MountingBox(
holes=holes,
length=self.root_height,
width=self.electronic_board.width,
centred=(True, True),
thickness=self.spacer_thickness,
flip_y=False,#self.flip,
generate_reverse_tags=True,
)
@submodel(name="spacer-s0-electronic2")
def spacer_s0_electronic_mount2(self) -> MountingBox:
"""
This one has circular holes
"""
def post(sketch: Cq.Sketch) -> Cq.Sketch:
return (
sketch
.push([(0,0)])
.rect(70, 130, mode='s')
)
return MountingBox(
holes=self.electronic_board.mount_holes,
hole_diam=self.electronic_board.mount_hole_diam,
length=self.root_height,
width=self.electronic_board.width,
centred=(True, True),
thickness=self.spacer_thickness,
flip_y=False,#self.flip,
generate_reverse_tags=True,
profile_callback=post,
)
@submodel(name="spacer-s0-shoulder-act")
def spacer_s0_shoulder_act(self) -> MountingBox:
return MountingBox(
holes=[Hole(x=0)],
hole_diam=self.shoulder_joint.actuator.back_hole_diam,
length=self.root_height,
width=10.0,
centred=(True, True),
thickness=self.spacer_thickness,
flip_y=self.flip,
generate_reverse_tags=True,
)
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.base_to_surface_thickness
#mid_spacer_loc = (
# Cq.Location.from2d(0, -self.shoulder_width/2) *
# self.shoulder_axle_loc *
# Cq.Location.rot2d(self.shoulder_joint.angle_neutral)
#)
axle_rotate = Cq.Location.rot2d(-self.shoulder_angle_neutral)
tags = [
("shoulder_left",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)),
("shoulder_right",
self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)),
("shoulder_act",
self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40, -30)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
("electronic_mount", Cq.Location.from2d(-35, 65, 60)),
]
result = extrude_with_markers(
self.profile_s0(top=top),
self.panel_thickness_s0,
tags,
reverse=top,
)
h = 0 if top else self.panel_thickness_s0
result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner")
result.copyWorkplane(Cq.Workplane('XZ')).moveTo(-self.base_width, self.panel_thickness_s0 - h).tagPlane("corner_left")
return result
@assembly()
def assembly_s0(
self,
ignore_electronics: bool=False) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s0(top=False), name="bot",
material=self.mat_panel, role=self.role_panel)
.addS(self.surface_s0(top=True), 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_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)
.addS(self.inner_shell_s0(), name="inner_shell",
material=self.mat_panel, role=self.role_panel)
.constrain("bot?corner_left", "inner_shell?bot", "Plane", param=0)
.constrain("top?corner_left", "inner_shell?top", "Plane", param=0)
)
for o, tag in [
(self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"),
(self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"),
(self.spacer_s0_shoulder_act().generate(), "shoulder_act"),
(self.spacer_s0_base().generate(), "base"),
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
]:
top_tag, bot_tag = "top", "bot"
if self.flip and tag.startswith("shoulder"):
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")
)
result.addS(
self.spacer_s0_electronic_mount2().generate(),
name="electronic_mount2",
role=Role.STRUCTURE | Role.CONNECTION,
material=self.mat_bracket)
for hole in self.electronic_board.mount_holes:
result.constrain(
f"electronic_mount?{hole.tag}",
f"electronic_mount2?{hole.rev_tag}",
"Plane")
if not ignore_electronics:
result.add(self.electronic_board.assembly(), name="electronic_board")
for hole in self.electronic_board.mount_holes:
assert hole.tag
nut_name = f"{hole.tag}_nut"
(
result
.add(self.electronic_board.nut.assembly(), name=nut_name)
.constrain(
f"{nut_name}?top",
f"electronic_mount?{hole.tag}",
"Plane", param=0
)
)
result.constrain(
f"electronic_mount2?{hole.tag}",
f'electronic_board/panel?{hole.rev_tag}',
"Plane",
)
return result.solve()
### s1, s2, s3 ###
def profile(self) -> Cq.Sketch:
"""
Generates profile from shoulder and above. Subclass should implement
"""
@target(name="profile-s2-bridge", kind=TargetKind.DXF)
def profile_s2_bridge(self) -> Optional[Cq.Sketch]:
return None
@target(name="profile-s3-extra", kind=TargetKind.DXF)
def profile_s3_extra(self) -> Optional[Cq.Sketch]:
"""
Extra element to be glued on s3. Not needed for left side
"""
return None
def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Optional[Cq.Sketch]:
"""
Creates a cutting polygon for removing the contraction part of a joint
"""
if not self.flip:
"""
No cutting needed on RHS
"""
return None
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 _joint_extension_cut_polygon(
self,
loc_bot: Cq.Location,
loc_top: Cq.Location,
height: float,
angle_span: float,
axle_pos: float,
bot: bool = True,
child: bool = False,
overestimate: float = 1.2,
median: float = 0.5,
) -> Cq.Sketch:
"""
A cut polygon to accomodate for joint extensions
"""
loc_ext = loc_bot if bot else loc_top
loc_tip = loc_top if bot else loc_bot
theta = math.radians(angle_span * (median if child else 1 - median))
if self.flip:
axle_pos = 1 - axle_pos
y_sign = -1 if bot else 1
sign = -1 if child else 1
dh = axle_pos * height * (overestimate - 1)
loc_left = loc_ext * Cq.Location.from2d(0, y_sign * dh)
loc_right = loc_left * Cq.Location.from2d(sign * height * overestimate * axle_pos * math.tan(theta), 0)
return (
Cq.Sketch()
.segment(
loc_tip.to2d_pos(),
loc_left.to2d_pos(),
)
.segment(
loc_left.to2d_pos(),
loc_right.to2d_pos(),
)
.segment(
loc_right.to2d_pos(),
loc_tip.to2d_pos(),
)
.assemble()
)
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_from_disk_joint(
self,
joint: ElbowJoint,
segment_thickness: float,
child: bool=False,
) -> MountingBox:
sign = 1 if child else -1
holes = [
Hole(sign * x, tag=tag)
for x, tag in joint.hole_loc_tags()
]
tongue_thickness = joint.disk_joint.tongue_thickness
carve_width = joint.lip_side_depression_width
assert carve_width >= self.light_strip.width
carve_height = (segment_thickness - tongue_thickness) / 2
assert carve_height >= self.light_strip.height
def carve_sides(profile):
dy = (segment_thickness + tongue_thickness) / 4
return (
profile
.push([(0,-dy), (0,dy)])
.rect(carve_width, carve_height, mode='s')
)
# FIXME: Carve out the sides so light can pass through
mbox = MountingBox(
length=joint.lip_length,
width=segment_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=joint.hole_diam,
centred=(True, True),
centre_left_right_tags=True,
profile_callback=carve_sides,
)
return mbox
def _actuator_mount(self, thickness: float, joint: ElbowJoint) -> MountingBox:
def post(sketch: Cq.Sketch) -> Cq.Sketch:
x = thickness / 2 - self.light_strip.height / 2
w = self.light_strip.height
h = self.light_strip.width
return (
sketch
.push([(x, 0), (-x, 0)])
.rect(w, h, mode='s')
#.push([(0, x), (0, -x)])
#.rect(h, w, mode='s')
)
return MountingBox(
length=thickness,
width=thickness,
thickness=self.spacer_thickness,
holes=[Hole(x=0,y=0)],
centred=(True, True),
hole_diam=joint.hole_diam,
centre_left_right_tags=True,
profile_callback=post,
)
@target(name="profile-s1", kind=TargetKind.DXF)
def profile_s1(self) -> Cq.Sketch:
cut_poly = self._joint_extension_cut_polygon(
loc_bot=self.elbow_bot_loc,
loc_top=self.elbow_top_loc,
height=self.elbow_height,
angle_span=self.elbow_joint.motion_span,
axle_pos=self.elbow_axle_pos,
bot=not self.elbow_joint.flip,
median=self.elbow_joint_overlap_median,
child=False,
).reset().polygon(self._mask_elbow(), mode='a')
profile = (
self.profile()
.reset()
.push([self.elbow_axle_loc.to2d_pos()])
.each(lambda _: cut_poly, mode='i')
#.polygon(self._mask_elbow(), mode='i')
)
return profile
def surface_s1(self, front: bool = True) -> Cq.Workplane:
rot_elbow = Cq.Location.rot2d(self.elbow_rotate)
loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc()
tags = [
("shoulder",
Cq.Location((0, self.shoulder_height / 2, 0)) *
self.shoulder_joint.child_lip_loc()),
("elbow", self.elbow_axle_loc * loc_elbow),
("elbow_act", self.elbow_axle_loc * rot_elbow *
self.elbow_joint.actuator_mount_loc()),
]
profile = self.profile_s1()
return extrude_with_markers(
profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s1-rod")
def spacer_s1_rod(self) -> MountingBox:
return MountingBox(
length=self.s1_thickness,
width=self.rod_width,
thickness=self.panel_thickness,
)
@submodel(name="spacer-s1-shoulder")
def spacer_s1_shoulder(self) -> MountingBox:
sign = 1#-1 if self.flip else 1
holes = [
Hole(x=sign * x)
for x in self.shoulder_joint.child_conn_hole_pos
]
return MountingBox(
length=self.shoulder_joint.child_lip_height,
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
centred=(True, True),
hole_diam=self.shoulder_joint.child_conn_hole_diam,
centre_left_right_tags=True,
centre_bot_top_tags=True,
)
@submodel(name="spacer-s1-elbow")
def spacer_s1_elbow(self) -> MountingBox:
return self._spacer_from_disk_joint(
joint=self.elbow_joint,
segment_thickness=self.s1_thickness,
)
@submodel(name="spacer-s1-elbow-act")
def spacer_s1_elbow_act(self) -> MountingBox:
return self._actuator_mount(
thickness=self.s1_thickness,
joint=self.elbow_joint
)
@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 o, t in [
(self.spacer_s1_shoulder(), "shoulder"),
(self.spacer_s1_elbow(), "elbow"),
(self.spacer_s1_elbow_act(), "elbow_act"),
]:
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=True,
)
return result.solve()
@target(name="profile-s2", kind=TargetKind.DXF)
def profile_s2(self) -> Cq.Sketch:
# Calculates `(profile - (E - JE)) * (W + JW)`
cut_elbow = (
Cq.Sketch()
.polygon(self._mask_elbow())
.reset()
.boolean(self._joint_extension_cut_polygon(
loc_bot=self.elbow_bot_loc,
loc_top=self.elbow_top_loc,
height=self.elbow_height,
angle_span=self.elbow_joint.motion_span,
axle_pos=self.elbow_axle_pos,
bot=not self.elbow_joint.flip,
median=self.elbow_joint_overlap_median,
child=True,
), mode='s')
)
cut_wrist = (
Cq.Sketch()
.polygon(self._mask_wrist())
)
if self.flip:
poly = self._joint_extension_cut_polygon(
loc_bot=self.wrist_bot_loc,
loc_top=self.wrist_top_loc,
height=self.wrist_height,
angle_span=self.wrist_joint.motion_span,
axle_pos=self.wrist_axle_pos,
bot=not self.wrist_joint.flip,
median=self.wrist_joint_overlap_median,
child=False,
)
cut_wrist = (
cut_wrist
.reset()
.boolean(poly, mode='a')
)
profile = (
self.profile()
.reset()
.boolean(cut_elbow, mode='s')
.boolean(cut_wrist, mode='i')
)
return profile
def surface_s2(self, front: bool = True) -> Cq.Workplane:
rot_elbow = Cq.Location.rot2d(self.elbow_rotate)
loc_elbow = rot_elbow * self.elbow_joint.child_arm_loc()
rot_wrist = Cq.Location.rot2d(self.wrist_rotate)
loc_wrist = rot_wrist * self.wrist_joint.parent_arm_loc()
tags = [
("elbow", self.elbow_axle_loc * loc_elbow),
("elbow_act", self.elbow_axle_loc * rot_elbow *
self.elbow_joint.actuator_mount_loc(child=True)),
("wrist", self.wrist_axle_loc * loc_wrist),
("wrist_act", self.wrist_axle_loc * rot_wrist *
self.wrist_joint.actuator_mount_loc()),
# for mounting the bridge only
("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()
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
def surface_s2_bridge(self, front: bool = True) -> Optional[Cq.Workplane]:
profile = self.profile_s2_bridge()
if profile is None:
return None
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc()
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)),
]
return extrude_with_markers(
profile, self.panel_thickness, tags, reverse=not front)
@submodel(name="spacer-s2-rod")
def spacer_s2_rod(self) -> MountingBox:
return MountingBox(
length=self.s2_thickness,
width=self.rod_width,
thickness=self.panel_thickness,
)
@submodel(name="spacer-s2-elbow")
def spacer_s2_elbow(self) -> MountingBox:
return self._spacer_from_disk_joint(
joint=self.elbow_joint,
segment_thickness=self.s2_thickness,
child=True,
)
@submodel(name="spacer-s2-elbow-act")
def spacer_s2_elbow_act(self) -> MountingBox:
return self._actuator_mount(
thickness=self.s2_thickness,
joint=self.elbow_joint
)
@submodel(name="spacer-s2-wrist")
def spacer_s2_wrist(self) -> MountingBox:
return self._spacer_from_disk_joint(
joint=self.wrist_joint,
segment_thickness=self.s2_thickness,
)
@submodel(name="spacer-s2-wrist-act")
def spacer_s2_wrist_act(self) -> MountingBox:
return self._actuator_mount(
thickness=self.s2_thickness,
joint=self.wrist_joint
)
@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)
)
bridge_front = self.surface_s2_bridge(front=True)
bridge_back = self.surface_s2_bridge(front=False)
if bridge_front:
(
result
.addS(bridge_front, name="bridge_front",
material=self.mat_panel, role=self.role_panel)
.constrain("front?wrist_bot", "bridge_front?wrist_bot", "Plane")
.constrain("front?wrist_top", "bridge_front?wrist_top", "Plane")
)
if bridge_back:
(
result
.addS(bridge_back, name="bridge_back",
material=self.mat_panel, role=self.role_panel)
.constrain("back?wrist_bot", "bridge_back?wrist_bot", "Plane")
.constrain("back?wrist_top", "bridge_back?wrist_top", "Plane")
)
for o, t in [
(self.spacer_s2_elbow(), "elbow"),
(self.spacer_s2_elbow_act(), "elbow_act"),
(self.spacer_s2_wrist(), "wrist"),
(self.spacer_s2_wrist_act(), "wrist_act"),
]:
is_parent = t.startswith("elbow")
self._assembly_insert_spacer(
result,
o.generate(),
point_tag=t,
flipped=True,#is_parent,
)
return result.solve()
@target(name="profile-s3", kind=TargetKind.DXF)
def profile_s3(self) -> Cq.Sketch:
cut_wrist = (
Cq.Sketch()
.polygon(self._mask_wrist())
)
if self.flip:
poly = self._joint_extension_cut_polygon(
loc_bot=self.wrist_bot_loc,
loc_top=self.wrist_top_loc,
height=self.wrist_height,
angle_span=self.wrist_joint.motion_span,
axle_pos=self.wrist_axle_pos,
bot=not self.wrist_joint.flip,
median=self.wrist_joint_overlap_median,
child=True,
)
cut_wrist = (
cut_wrist
.boolean(poly, mode='s')
)
profile = (
self.profile()
.boolean(cut_wrist, mode='s')
)
return profile
def surface_s3(self,
front: bool = True) -> Cq.Workplane:
rot_wrist = Cq.Location.rot2d(self.wrist_rotate)
loc_wrist = rot_wrist * self.wrist_joint.child_arm_loc()
tags = [
("wrist", self.wrist_axle_loc * loc_wrist),
("wrist_act", self.wrist_axle_loc * rot_wrist *
self.wrist_joint.actuator_mount_loc(child=True)),
("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)
def surface_s3_extra(self,
front: bool = True) -> Optional[Cq.Workplane]:
profile = self.profile_s3_extra()
if profile is None:
return None
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc()
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)),
]
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front)
@submodel(name="spacer-s3-rod")
def spacer_s3_rod(self) -> MountingBox:
return MountingBox(
length=self.s3_thickness,
width=self.rod_width,
thickness=self.panel_thickness,
)
@submodel(name="spacer-s3-wrist")
def spacer_s3_wrist(self) -> MountingBox:
return self._spacer_from_disk_joint(
joint=self.wrist_joint,
segment_thickness=self.s3_thickness,
child=True,
)
@submodel(name="spacer-s3-wrist-act")
def spacer_s3_wrist_act(self) -> MountingBox:
return self._actuator_mount(
thickness=self.s3_thickness,
joint=self.wrist_joint
)
@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)
)
if not self.flip:
(
result
.addS(self.surface_s3_extra(front=True), name="extra_front",
material=self.mat_panel, role=self.role_panel)
.constrain("front?wrist_bot", "extra_front?wrist_bot", "Plane")
.constrain("front?wrist_top", "extra_front?wrist_top", "Plane")
.addS(self.surface_s3_extra(front=False), name="extra_back",
material=self.mat_panel, role=self.role_panel)
.constrain("back?wrist_bot", "extra_back?wrist_bot", "Plane")
.constrain("back?wrist_top", "extra_back?wrist_top", "Plane")
)
self._assembly_insert_spacer(
result,
self.spacer_s3_wrist_act().generate(),
point_tag="wrist_act",
flipped=True,
)
self._assembly_insert_spacer(
result,
self.spacer_s3_wrist().generate(),
point_tag="wrist",
flipped=True,
)
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,
ignore_fasteners: bool = False,
ignore_electronics: bool = False,
ignore_actuators: bool = False,
) -> Cq.Assembly():
assert 0 <= elbow_wrist_deflection <= 1
assert 0 <= shoulder_deflection <= 1
assert 0 <= fastener_pos <= 1
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(
ignore_electronics=ignore_electronics
), name="s0")
if not ignore_electronics:
tag_act = "shoulder_act"
tag_bolt = "shoulder_act_bolt"
tag_nut = "shoulder_act_nut"
tag_bracket = "shoulder_act_bracket"
winch = self.shoulder_joint.winch
(
result
.add(winch.actuator.assembly(pos=0), name=tag_act)
.add(winch.bracket.assembly(), name=tag_bracket)
.add(winch.bolt.assembly(), name=tag_bolt)
.add(winch.nut.assembly(), name=tag_nut)
.constrain(f"{tag_bolt}?root", f"{tag_bracket}?conn_top",
"Plane", param=0)
.constrain(f"{tag_nut}?bot", f"{tag_bracket}?conn_bot",
"Plane")
.constrain(f"{tag_act}/back?conn", f"{tag_bracket}?conn_mid",
"Plane", param=0)
.constrain("s0/shoulder_act?conn0", f"{tag_bracket}?conn_side",
"Plane")
# Directional constraints should be provided by the line
.constrain(f"{tag_bracket}?conn_mid", "s0/shoulder_act?top", "Axis", param=0)
.constrain(f"{tag_act}/back?dir", "s0/shoulder_act?conn0", "Axis", param=0)
)
if "root" in parts:
result.addS(self.root_joint.assembly(
offset=root_offset,
fastener_pos=fastener_pos,
ignore_fasteners=ignore_fasteners,
), name="root")
result.constrain("root/parent", "Fixed")
if "s0" in parts and "root" in parts:
(
result
.constrain("s0/base?conn0", "root/child?conn0", "Point")
.constrain("s0/base?conn1", "root/child?conn1", "Point")
.constrain("s0/base?conn2", "root/child?conn2", "Point")
#.constrain("s0/base?conn3", "root/child?conn3", "Point")
)
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,
ignore_fasteners=ignore_fasteners), name="shoulder")
if "s0" in parts and "shoulder" in parts:
for i in range(len(self.shoulder_joint.parent_conn_hole_pos)):
(
result
.constrain(f"s0/shoulder_left?conn_top{i}", f"shoulder/parent_{tag_top}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder_left?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_left?conn{i}", "Plane")
.constrain(f"s0/shoulder_right?conn_top{i}", f"shoulder/parent_{tag_top}/lip_right?conn{i}", "Plane")
.constrain(f"s0/shoulder_right?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_right?conn{i}", "Plane")
)
if "s1" in parts:
result.add(self.assembly_s1(), name="s1")
if "s1" in parts and "shoulder" in parts:
for i in range(len(self.shoulder_joint.child_conn_hole_pos)):
result.constrain(f"s1/shoulder?conn{i}", f"shoulder/child/lip?conn{i}", "Plane")
if "elbow" in parts:
angle = self.elbow_joint.motion_span * elbow_wrist_deflection
result.add(self.elbow_joint.assembly(
angle=angle,
ignore_actuators=ignore_actuators), name="elbow")
if "s1" in parts and "elbow" in parts:
for _, tag in self.elbow_joint.hole_loc_tags():
result.constrain(
f"s1/elbow?{tag}",
f"elbow/parent_upper/lip?{tag}", "Plane")
#if not ignore_actuators:
# result.constrain(
# "elbow/bracket_back?conn_side",
# "s1/elbow_act?conn0",
# "Plane")
if "s2" in parts:
result.add(self.assembly_s2(), name="s2")
if "s2" in parts and "elbow" in parts:
for _, tag in self.elbow_joint.hole_loc_tags():
result.constrain(
f"s2/elbow?{tag}",
f"elbow/child/lip?{tag}", "Plane")
if "wrist" in parts:
angle = self.wrist_joint.motion_span * elbow_wrist_deflection
result.add(self.wrist_joint.assembly(
angle=angle,
ignore_actuators=ignore_actuators), name="wrist")
if "s2" in parts and "wrist" in parts:
for _, tag in self.wrist_joint.hole_loc_tags():
result.constrain(
f"s2/wrist?{tag}",
f"wrist/parent_upper/lip?{tag}", "Plane")
if "s3" in parts:
result.add(self.assembly_s3(), name="s3")
if "s3" in parts and "wrist" in parts:
for _, tag in self.wrist_joint.hole_loc_tags():
result.constrain(
f"s3/wrist?{tag}",
f"wrist/child/lip?{tag}", "Plane")
#if not ignore_actuators:
# result.constrain(
# "wrist/bracket_back?conn_side",
# "s2/wrist_act?conn0",
# "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
elbow_rotate: float = 10.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=55,
spring_angle_at_0=75,
**ELBOW_DISK_PARAMS,
),
flexor_offset_angle=15,
flexor_mount_angle_child=-75,
flexor_child_arm_radius=None,
angle_neutral=10.0,
flip=False,
**ELBOW_PARAMS
))
wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0)
wrist_height: float = 60.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
spring_angle_at_0=120,
**WRIST_DISK_PARAMS,
),
flip=True,
angle_neutral=-20.0,
child_arm_radius=23.0,
parent_arm_radius=30.0,
flexor_line_length=50.0,
flexor_line_slack=3.0,
flexor_offset_angle=45.0,
flexor_child_arm_radius=None,
flexor_mount_angle_parent=20,
flexor_mount_angle_child=-40,
hole_pos=[10, 20],
lip_length=50,
actuator=LINEAR_ACTUATOR_10,
#flexor_pos_smaller=False,
**WRIST_PARAMS
))
# Extends from the wrist to the tip of the arrow
arrow_height: float = 300
arrow_angle: float = -8
# Underapproximate the wrist tangent angle to leave no gaps on the blade
blade_wrist_approx_tangent_angle: float = 40.0
# Some overlap needed to glue the two sides
blade_overlap_angle: float = -1
blade_hole_angle: float = 3
blade_hole_diam: float = 12.0
blade_hole_heights: list[float] = field(default_factory=lambda: [230, 260])
blade_angle: float = 7
# 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
flip: bool = False
elbow_axle_pos: float = 0.4
wrist_axle_pos: float = 0.0
elbow_joint_overlap_median: float = 0.35
wrist_joint_overlap_median: float = 0.5
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
assert 0 > self.blade_overlap_angle > self.arrow_angle
assert 0 < self.blade_hole_angle < self.blade_angle
assert self.blade_wrist_approx_tangent_angle <= self.wrist_bot_loc.to2d_rot()
@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 _child_joint_extension_profile(
self,
axle_loc: Cq.Location,
radius: float,
angle_span: float,
bot: bool = False) -> Cq.Sketch:
"""
Creates a sector profile which accomodates extension
"""
# leave some margin for gluing
margin = 5
sign = -1 if bot else 1
axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90)
loc_h = Cq.Location.from2d(radius, 0)
loc_offset = axle_loc * Cq.Location.from2d(0, margin)
start = axle_loc * loc_h
mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h
end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h
return (
Cq.Sketch()
.segment(
loc_offset.to2d_pos(),
start.to2d_pos(),
)
.arc(
start.to2d_pos(),
mid.to2d_pos(),
end.to2d_pos(),
)
.segment(
end.to2d_pos(),
axle_loc.to2d_pos(),
)
.segment(
axle_loc.to2d_pos(),
loc_offset.to2d_pos(),
)
.assemble()
)
@target(name="profile-s2-bridge", kind=TargetKind.DXF)
def profile_s2_bridge(self) -> Cq.Sketch:
"""
This extension profile is required to accomodate the awkward shaped
joint next to the scythe
"""
profile = self._child_joint_extension_profile(
axle_loc=self.wrist_axle_loc,
radius=self.wrist_height,
angle_span=self.wrist_joint.motion_span,
bot=False,
)
return profile
def profile_s3_extra(self) -> Cq.Sketch:
"""
Implements the blade part on Nue's wing
"""
left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1)
hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle)
right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle)
h_loc = Cq.Location.from2d(0, self.arrow_height)
# Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?)
theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle)
theta_b = math.radians(self.blade_angle)
h_blade = math.sin(theta_wp) / math.sin(math.pi - theta_b - theta_wp) * self.arrow_height
h_blade_loc = Cq.Location.from2d(0, h_blade)
return (
Cq.Sketch()
.segment(
self.arrow_bot_loc.to2d_pos(),
(left_bot_loc * h_loc).to2d_pos(),
)
.segment(
(self.arrow_bot_loc * h_loc).to2d_pos(),
)
.segment(
(right_bot_loc * h_blade_loc).to2d_pos(),
)
.close()
.assemble()
.reset()
.push([
(hole_bot_loc * Cq.Location.from2d(0, h)).to2d_pos()
for h in self.blade_hole_heights
])
.circle(self.blade_hole_diam / 2, mode='s')
)
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, 105.0, 0.0)
elbow_height: float = 95.0
elbow_rotate: float = 15.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
spring_angle_at_0=100,
movement_angle=50,
**ELBOW_DISK_PARAMS,
),
angle_neutral=30.0,
flexor_mount_angle_child=220,
flexor_mount_angle_parent=0,
flexor_line_length=50.0,
flexor_line_slack=10.0,
#flexor_line_length=0.0,
#flexor_line_slack=0.0,
flexor_offset_angle=0,
flexor_child_angle_fix=85,
flexor_parent_angle_fix=None,
flexor_child_arm_radius=50.0,
parent_arm_radius=50.0,
child_arm_radius=50.0,
flexor_pos_smaller=False,
flip=True,
**ELBOW_PARAMS
))
elbow_axle_pos: float = 0.53
elbow_joint_overlap_median: float = 0.5
wrist_angle: float = 0.0
wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0)
wrist_height: float = 43.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
tongue_thickness=8.0,
**WRIST_DISK_PARAMS,
),
flip=False,
hole_pos=[10],
lip_length=30,
child_arm_radius=23.0,
parent_arm_radius=30.0,
flexor_offset_angle=0.0,
flexor_child_arm_radius=None,
flexor_line_length=50.0,
flexor_line_slack=1.0,
actuator=LINEAR_ACTUATOR_10,
**WRIST_PARAMS
))
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
wrist_axle_pos: float = 0.5
wrist_joint_overlap_median: float = 0.5
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.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() * 0.7 + 30.0
self.wrist_rotate = -self.wrist_joint.angle_neutral
self.shoulder_joint.flip = True
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_x, elbow_top_y = self.elbow_top_loc.to2d_pos()
_, wrist_bot_y = self.wrist_bot_loc.to2d_pos()
wrist_top_x, wrist_top_y = self.wrist_top_loc.to2d_pos()
return [
(0, -l),
(elbow_bot_x, wrist_bot_y),
self.wrist_bot_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
(wrist_top_x, wrist_top_y + l),
(elbow_top_x, elbow_top_y + l),
(0, l),
]