Cosplay/nhf/touhou/houjuu_nue/wing.py

1274 lines
47 KiB
Python
Raw Normal View History

"""
This file describes the shapes of the wing shells. The joints are defined in
`__init__.py`.
"""
2024-06-24 16:13:15 -07:00
import math
2024-07-11 16:02:54 -07:00
from enum import Enum
from dataclasses import dataclass, field
from typing import Mapping, Tuple, Optional
2024-06-24 16:13:15 -07:00
import cadquery as Cq
from nhf import Material, Role
2024-07-16 15:42:39 -07:00
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
2024-07-04 00:42:14 -07:00
from nhf.parts.joints import HirthJoint
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_50, ElectronicBoard
import nhf.utils
2024-06-24 16:13:15 -07:00
2024-07-16 14:25:17 -07:00
@dataclass(kw_only=True)
class WingProfile(Model):
2024-07-09 19:57:54 -07:00
name: str = "wing"
2024-07-07 21:01:40 -07:00
base_width: float = 80.0
2024-07-19 16:13:33 -07:00
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
2024-07-14 00:47:44 -07:00
panel_thickness: float = 25.4 / 16
# 1/4" acrylic for the spacer. Anything thinner would threathen structural
# strength
spacer_thickness: float = 25.4 / 4
2024-07-14 17:56:02 -07:00
2024-07-16 15:42:39 -07:00
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
))
shoulder_angle_bias: float = 0.0
2024-07-17 00:30:41 -07:00
shoulder_width: float = 36.0
2024-07-19 14:06:13 -07:00
shoulder_tip_x: float = -260.0
shoulder_tip_y: float = 165.0
2024-07-22 15:20:09 -07:00
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
2024-07-09 19:57:54 -07:00
s0_hole_loc: Cq.Location = Cq.Location.from2d(-25, 33)
s0_hole_diam: float = 15.0
s0_top_hole: bool = False
s0_bot_hole: bool = True
electronic_board: ElectronicBoard = field(default_factory=lambda: ElectronicBoard())
s1_thickness: float = 25.0
2024-07-09 21:13:16 -07:00
2024-07-16 14:25:17 -07:00
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=55,
),
2024-07-22 09:49:16 -07:00
hole_diam=4.0,
2024-07-18 14:03:01 -07:00
angle_neutral=15.0,
actuator=LINEAR_ACTUATOR_50,
flexor_offset_angle=-15,
2024-07-16 14:25:17 -07:00
))
2024-07-17 10:22:59 -07:00
# Distance between the two spacers on the elbow, halved
elbow_h2: float = 5.0
2024-07-09 21:13:16 -07:00
2024-07-16 14:25:17 -07:00
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
disk_joint=DiskJoint(
movement_angle=30,
2024-07-17 01:22:05 -07:00
radius_disk=13.0,
radius_housing=15.0,
2024-07-16 14:25:17 -07:00
),
2024-07-22 09:49:16 -07:00
hole_pos=[10],
lip_length=30,
2024-07-17 14:47:22 -07:00
child_arm_radius=23.0,
2024-07-17 12:11:08 -07:00
parent_arm_radius=30.0,
2024-07-17 14:47:22 -07:00
hole_diam=4.0,
2024-07-20 22:55:43 -07:00
angle_neutral=-30.0,
actuator=LINEAR_ACTUATOR_10,
2024-07-16 14:25:17 -07:00
))
2024-07-17 10:22:59 -07:00
# Distance between the two spacers on the elbow, halved
wrist_h2: float = 5.0
2024-07-09 21:13:16 -07:00
mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT
mat_bracket: Material = Material.ACRYLIC_TRANSPARENT
mat_hs_joint: Material = Material.PLASTIC_PLA
role_panel: Role = Role.STRUCTURE
2024-07-16 17:18:28 -07:00
# Subclass must populate
elbow_bot_loc: Cq.Location
2024-07-16 17:18:28 -07:00
elbow_height: float
wrist_bot_loc: Cq.Location
2024-07-16 17:18:28 -07:00
wrist_height: float
2024-07-18 14:09:53 -07:00
elbow_rotate: float = -5.0
2024-07-20 22:55:43 -07:00
wrist_rotate: float = -30.0
# Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side)
2024-07-22 15:02:26 -07:00
elbow_axle_pos: float = 0.5
2024-07-22 09:49:16 -07:00
wrist_axle_pos: float = 0.0
2024-07-16 17:18:28 -07:00
# False for the right side, True for the left side
flip: bool
2024-07-16 17:18:28 -07:00
2024-07-09 21:13:16 -07:00
def __post_init__(self):
super().__init__(name=self.name)
2024-07-09 21:13:16 -07:00
2024-07-22 15:02:26 -07:00
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)
if self.flip:
self.elbow_axle_pos = 1 - self.elbow_axle_pos
self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height * self.elbow_axle_pos)
if self.flip:
2024-07-22 09:49:16 -07:00
self.wrist_axle_pos = 1 - self.wrist_axle_pos
self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos)
2024-07-11 16:02:54 -07:00
2024-07-19 14:06:13 -07:00
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
2024-07-19 16:13:33 -07:00
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
2024-07-22 15:02:26 -07:00
2024-07-19 15:06:57 -07:00
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
"""
2024-07-22 09:49:16 -07:00
extra = 2 * self.panel_thickness if self.flip else 0
return self.s1_thickness - 2 * self.panel_thickness - extra
2024-07-16 22:26:06 -07:00
@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
2024-07-16 17:18:28 -07:00
@property
def shoulder_height(self) -> float:
return self.shoulder_joint.height
2024-07-17 00:30:41 -07:00
def outer_profile_s0(self) -> Cq.Sketch:
"""
2024-07-22 15:20:09 -07:00
The outer boundary of s0 top/bottom slots
2024-07-17 00:30:41 -07:00
"""
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
2024-07-17 00:30:41 -07:00
return (
Cq.Sketch()
.spline([
(0, 0),
(-30.0, 80.0),
(tip_x, tip_y)
])
2024-07-17 00:30:41 -07:00
#.segment(
# (tip_x, tip_y),
# (tip_x - 10, tip_y),
#)
)
2024-07-22 15:20:09 -07:00
def inner_profile_s0(self) -> 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
return Cq.Edge.makeBezier(
[
Cq.Vector(*p)
for p in [
(tip_x, tip_y - sw),
(tip_x + dx2, tip_y - sw + dy2),
(-self.base_width + dx1, dy1),
(-self.base_width, 0),
]
]
)
2024-07-17 00:30:41 -07:00
@property
def shoulder_angle_neutral(self) -> float:
"""
Returns the neutral angle of the shoulder
"""
2024-07-22 15:20:09 -07:00
result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x))
assert result >= 0
return result
2024-07-17 00:30:41 -07:00
@target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self, top: bool = True) -> Cq.Sketch:
2024-07-17 00:30:41 -07:00
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
2024-07-22 15:20:09 -07:00
dx2 = self.shoulder_tip_bezier_x
dy2 = self.shoulder_tip_bezier_y
dx1 = self.shoulder_base_bezier_x
dy1 = self.shoulder_base_bezier_y
2024-07-17 00:30:41 -07:00
sw = self.shoulder_width
sketch = (
self.outer_profile_s0()
.segment((-self.base_width, 0), (0, 0))
.segment(
(tip_x, tip_y),
2024-07-17 00:30:41 -07:00
(tip_x, tip_y - sw),
)
2024-07-22 15:20:09 -07:00
.bezier([
2024-07-17 00:30:41 -07:00
(tip_x, tip_y - sw),
2024-07-22 15:20:09 -07:00
(tip_x + dx2, tip_y - sw + dy2),
(-self.base_width + dx1, dy1),
(-self.base_width, 0),
2024-07-22 15:20:09 -07:00
])
.assemble()
2024-07-19 14:06:13 -07:00
.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):
sketch = (
sketch
.reset()
.push([self.s0_hole_loc.to2d_pos()])
.circle(self.s0_hole_diam / 2, mode='s')
)
return sketch
def outer_shell_s0(self) -> Cq.Workplane:
t = self.panel_thickness
2024-07-17 00:30:41 -07:00
profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals())
result = (
Cq.Workplane('XZ')
.rect(t, self.root_height + t*2, centered=(False, False))
2024-07-17 00:30:41 -07:00
.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
2024-07-22 15:20:09 -07:00
def inner_shell_s0(self) -> Cq.Workplane:
t = self.panel_thickness
#profile = Cq.Wire.assembleEdges(self.inner_profile_s0())
result = (
Cq.Workplane('XZ')
.rect(t, self.root_height + t*2, centered=(False, False))
.sweep(self.inner_profile_s0())
)
plane = result.copyWorkplane(Cq.Workplane('XZ'))
plane.moveTo(t, 0).tagPlane("bot")
plane.moveTo(t, self.root_height + t*2).tagPlane("top")
return result
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s0-shoulder")
def spacer_s0_shoulder(self) -> MountingBox:
"""
2024-07-19 16:13:33 -07:00
Shoulder side serves double purpose for mounting shoulder joint and
structural support
"""
holes = [
hole
2024-07-17 00:30:41 -07:00
for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos)
for hole in [
2024-07-17 00:30:41 -07:00
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,
2024-07-17 00:30:41 -07:00
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),
2024-07-16 17:18:28 -07:00
flip_y=self.flip,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox:
"""
2024-07-19 16:13:33 -07:00
Base side connects to H-S joint
"""
2024-07-19 15:06:57 -07:00
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,
2024-07-19 15:06:57 -07:00
width=self.root_joint.child_width,
thickness=self.spacer_thickness,
holes=holes,
2024-07-19 15:06:57 -07:00
hole_diam=self.root_joint.corner_hole_diam,
centred=(True, True),
2024-07-16 17:18:28 -07:00
flip_y=self.flip,
)
@submodel(name="spacer-s0-electronic")
2024-07-22 15:02:26 -07:00
def spacer_s0_electronic_mount(self) -> MountingBox:
2024-07-19 16:13:33 -07:00
return MountingBox(
2024-07-22 15:02:26 -07:00
holes=self.electronic_board.mount_holes,
hole_diam=self.electronic_board.mount_hole_diam,
2024-07-19 16:13:33 -07:00
length=self.root_height,
2024-07-22 15:02:26 -07:00
width=self.electronic_board.width,
centred=(True, True),
2024-07-19 16:13:33 -07:00
thickness=self.spacer_thickness,
2024-07-22 15:02:26 -07:00
flip_y=self.flip,
generate_reverse_tags=True,
2024-07-19 16:13:33 -07:00
)
def surface_s0(self, top: bool = False) -> Cq.Workplane:
2024-07-19 16:13:33 -07:00
base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10
2024-07-19 15:06:57 -07:00
base_dy = self.root_joint.hirth_joint.joint_height
2024-07-19 14:06:13 -07:00
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 = [
2024-07-19 14:06:13 -07:00
("shoulder",
self.shoulder_axle_loc *
self.shoulder_joint.parent_arm_loc() *
loc_tip),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
2024-07-22 15:02:26 -07:00
("electronic_mount", Cq.Location.from2d(-55, 75, 64)),
]
result = extrude_with_markers(
self.profile_s0(top=top),
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")
2024-07-22 15:20:09 -07:00
result.copyWorkplane(Cq.Workplane('XZ')).moveTo(-self.base_width, h).tagPlane("corner_left")
return result
2024-07-16 15:42:39 -07:00
@assembly()
2024-07-22 15:02:26 -07:00
def assembly_s0(self, ignore_detail: bool=False) -> 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",
2024-07-17 00:30:41 -07:00
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")
2024-07-22 15:20:09 -07:00
.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)
2024-07-22 15:20:09 -07:00
#.addS(self.inner_shell_s0(), name="inner_shell",
# material=self.mat_panel, role=self.role_panel)
#.constrain("bot?corner_left", "inner_shell?bot", "Point")
#.constrain("top?corner_left", "inner_shell?top", "Point")
)
for o, tag in [
(self.spacer_s0_shoulder().generate(), "shoulder"),
2024-07-19 16:13:33 -07:00
(self.spacer_s0_base().generate(), "base"),
2024-07-22 15:02:26 -07:00
(self.spacer_s0_electronic_mount().generate(), "electronic_mount"),
]:
2024-07-16 17:18:28 -07:00
top_tag, bot_tag = "top", "bot"
if self.flip:
top_tag, bot_tag = bot_tag, top_tag
(
result
.addS(o, name=tag,
role=Role.STRUCTURE | Role.CONNECTION,
material=self.mat_bracket)
2024-07-16 17:18:28 -07:00
.constrain(f"{tag}?{bot_tag}", f"bot?{tag}", "Plane")
.constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane")
.constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis")
)
2024-07-22 15:02:26 -07:00
if not ignore_detail:
result.add(self.electronic_board.assembly(), name="electronic_board")
for hole in self.electronic_board.mount_holes:
assert hole.tag
nut_name = f"electronic_board_{hole.tag}_nut"
(
result
.addS(
self.electronic_board.nut.assembly(),
name=nut_name)
.constrain(
f"electronic_mount?{hole.rev_tag}",
f'{nut_name}?top',
"Plane"
)
.constrain(
f"electronic_mount?{hole.tag}",
f'electronic_board/{hole.tag}_spacer?bot',
"Plane"
)
)
return result.solve()
### s1, s2, s3 ###
2024-07-16 17:18:28 -07:00
def profile(self) -> Cq.Sketch:
"""
Generates profile from shoulder and above. Subclass should implement
2024-07-16 17:18:28 -07:00
"""
@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
])
)
2024-07-22 09:49:16 -07:00
def _child_joint_extension_profile(
2024-07-20 22:55:43 -07:00
self,
axle_loc: Cq.Location,
radius: float,
angle_span: float,
bot: bool = False) -> Cq.Sketch:
"""
Creates a sector profile which accomodates extension
"""
2024-07-20 22:55:43 -07:00
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)
2024-07-20 22:55:43 -07:00
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(
axle_loc.to2d_pos(),
start.to2d_pos(),
)
.arc(
start.to2d_pos(),
mid.to2d_pos(),
end.to2d_pos(),
)
.segment(
end.to2d_pos(),
axle_loc.to2d_pos(),
)
.assemble()
)
def _parent_joint_extension_profile(
self,
loc_axle: Cq.Location,
loc_bot: Cq.Location,
loc_top: Cq.Location,
angle_span: float,
bot: bool = True
) -> Cq.Sketch:
"""
Generates a sector-like profile on the child side of a panel to
accomodate for joint rotation
"""
sign = -1 if bot else 1
loc_tip = loc_top if bot else loc_bot
loc_arc_right = loc_bot if bot else loc_top
loc_rel_arc_right = loc_axle.inverse * loc_arc_right
loc_arc_left = loc_axle * Cq.Location.rot2d(sign * angle_span) * loc_rel_arc_right
loc_arc_middle = loc_axle * Cq.Location.rot2d(sign * angle_span / 2) * loc_rel_arc_right
return (
Cq.Sketch()
.segment(
loc_tip.to2d_pos(),
loc_arc_right.to2d_pos(),
)
.arc(
loc_arc_right.to2d_pos(),
loc_arc_middle.to2d_pos(),
loc_arc_left.to2d_pos(),
)
.segment(
loc_tip.to2d_pos(),
loc_arc_left.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,
2024-07-17 10:22:59 -07:00
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
2024-07-17 10:22:59 -07:00
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)
)
2024-07-11 16:02:54 -07:00
def _mask_elbow(self) -> list[Tuple[float, float]]:
"""
Polygon shape to mask out parts above the elbow
"""
def _mask_wrist(self) -> list[Tuple[float, float]]:
2024-07-17 01:19:17 -07:00
"""
Polygon shape to mask wrist
"""
2024-07-11 16:02:54 -07:00
2024-07-17 10:22:59 -07:00
def spacer_of_joint(
self,
joint: ElbowJoint,
segment_thickness: float,
dx: float) -> MountingBox:
2024-07-17 10:22:59 -07:00
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
2024-07-11 16:02:54 -07:00
2024-07-16 15:42:39 -07:00
@target(name="profile-s1", kind=TargetKind.DXF)
2024-07-11 16:02:54 -07:00
def profile_s1(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='i')
)
return profile
def surface_s1(self, front: bool = True) -> Cq.Workplane:
shoulder_h = self.shoulder_joint.child_height
h = (self.shoulder_joint.height - shoulder_h) / 2
2024-07-11 16:02:54 -07:00
tags_shoulder = [
("shoulder_bot", Cq.Location.from2d(0, h, 90)),
("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)),
2024-07-11 16:02:54 -07:00
]
2024-07-17 10:22:59 -07:00
h = self.elbow_height / 2
2024-07-18 14:03:01 -07:00
loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc()
2024-07-11 16:02:54 -07:00
tags_elbow = [
("elbow_bot", self.elbow_axle_loc * loc_elbow *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, -self.elbow_h2)),
("elbow_top", self.elbow_axle_loc * loc_elbow *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, self.elbow_h2)),
2024-07-11 16:02:54 -07:00
]
profile = self.profile_s1()
tags = tags_shoulder + tags_elbow
return extrude_with_markers(
2024-07-17 12:11:08 -07:00
profile, self.panel_thickness, tags, reverse=front)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s1-shoulder")
def spacer_s1_shoulder(self) -> MountingBox:
holes = [
Hole(x)
for x in self.shoulder_joint.child_conn_hole_pos
]
return MountingBox(
length=50.0, # FIXME: magic
width=self.s1_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=self.shoulder_joint.child_conn_hole_diam,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s1-elbow")
def spacer_s1_elbow(self) -> MountingBox:
2024-07-17 10:22:59 -07:00
return self.spacer_of_joint(
joint=self.elbow_joint,
segment_thickness=self.s1_thickness,
dx=self.elbow_h2,
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s1(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s1(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s1(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
for t in ["shoulder_bot", "shoulder_top", "elbow_bot", "elbow_top"]:
is_top = t.endswith("_top")
is_parent = t.startswith("shoulder")
o = self.spacer_s1_shoulder().generate() if is_parent else self.spacer_s1_elbow().generate()
self._assembly_insert_spacer(
result,
o,
point_tag=t,
2024-07-17 12:11:08 -07:00
flipped=not is_top,
)
return result.solve()
2024-07-11 16:02:54 -07:00
2024-07-16 15:42:39 -07:00
@target(name="profile-s2", kind=TargetKind.DXF)
2024-07-11 16:02:54 -07:00
def profile_s2(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_elbow(), mode='s')
.reset()
.polygon(self._mask_wrist(), mode='i')
.reset()
.push([self.elbow_axle_loc])
.each(lambda loc: self._parent_joint_extension_profile(
loc,
self.elbow_bot_loc,
self.elbow_top_loc,
self.elbow_joint.motion_span,
bot=not self.flip,
), mode='a')
2024-07-11 16:02:54 -07:00
)
return profile
def surface_s2(self, front: bool = True) -> Cq.Workplane:
loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip)
2024-07-12 11:04:28 -07:00
tags_elbow = [
("elbow_bot", self.elbow_axle_loc * loc_elbow *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, self.elbow_h2)),
("elbow_top", self.elbow_axle_loc * loc_elbow *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, -self.elbow_h2)),
2024-07-12 11:04:28 -07:00
]
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc()
2024-07-12 11:04:28 -07:00
tags_wrist = [
("wrist_bot", self.wrist_axle_loc * loc_wrist *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, -self.wrist_h2)),
("wrist_top", self.wrist_axle_loc * loc_wrist *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, self.wrist_h2)),
2024-07-12 11:04:28 -07:00
]
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
2024-07-20 22:55:43 -07:00
@target(name="profile-s2-bridge", kind=TargetKind.DXF)
def profile_s2_bridge(self) -> Cq.Workplane:
2024-07-22 09:49:16 -07:00
# FIXME: Leave some margin here so we can glue the panels
2024-07-20 22:55:43 -07:00
# Generates the extension profile, which is required on both sides
2024-07-22 09:49:16 -07:00
profile = self._child_joint_extension_profile(
2024-07-20 22:55:43 -07:00
axle_loc=self.wrist_axle_loc,
radius=self.wrist_height * (0.5 if self.flip else 1),
angle_span=self.wrist_joint.motion_span,
bot=self.flip,
2024-07-20 22:55:43 -07:00
)
# Generates the contraction (cut) profile. only required on the left
if self.flip:
extra = (
self.profile()
.reset()
.push([self.wrist_axle_loc])
.each(self._wrist_joint_retract_cut_polygon, mode='i')
)
profile = (
profile
.push([self.wrist_axle_loc])
.each(lambda _: extra, mode='a')
)
return profile
def surface_s2_bridge(self, front: bool = True) -> Cq.Workplane:
profile = self.profile_s2_bridge()
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)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s2-elbow")
def spacer_s2_elbow(self) -> MountingBox:
2024-07-17 10:22:59 -07:00
return self.spacer_of_joint(
joint=self.elbow_joint,
segment_thickness=self.s2_thickness,
dx=self.elbow_h2,
)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s2-wrist")
def spacer_s2_wrist(self) -> MountingBox:
2024-07-17 10:22:59 -07:00
return self.spacer_of_joint(
joint=self.wrist_joint,
segment_thickness=self.s2_thickness,
dx=self.wrist_h2,
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s2(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s2(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s2(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
2024-07-20 22:55:43 -07:00
.addS(self.surface_s2_bridge(front=True), 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")
.addS(self.surface_s2_bridge(front=False), 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 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,
2024-07-17 12:11:08 -07:00
flipped=is_top == is_parent,
2024-07-18 14:03:01 -07:00
#rotate=not is_parent,
)
return result.solve()
2024-07-12 11:04:28 -07:00
2024-07-16 15:42:39 -07:00
@target(name="profile-s3", kind=TargetKind.DXF)
2024-07-11 16:02:54 -07:00
def profile_s3(self) -> Cq.Sketch:
profile = (
self.profile()
.reset()
.polygon(self._mask_wrist(), mode='s')
)
return profile
2024-07-12 11:04:28 -07:00
def surface_s3(self,
front: bool = True) -> Cq.Workplane:
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip)
2024-07-12 11:04:28 -07:00
tags = [
("wrist_bot", self.wrist_axle_loc * loc_wrist *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, self.wrist_h2)),
("wrist_top", self.wrist_axle_loc * loc_wrist *
2024-07-18 14:03:01 -07:00
Cq.Location.from2d(0, -self.wrist_h2)),
2024-07-12 11:04:28 -07:00
]
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(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)),
]
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front)
2024-07-16 15:42:39 -07:00
@submodel(name="spacer-s3-wrist")
def spacer_s3_wrist(self) -> MountingBox:
2024-07-17 10:22:59 -07:00
return self.spacer_of_joint(
joint=self.wrist_joint,
segment_thickness=self.s3_thickness,
dx=self.wrist_h2,
)
2024-07-16 15:42:39 -07:00
@assembly()
def assembly_s3(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.addS(self.surface_s3(front=True), name="front",
material=self.mat_panel, role=self.role_panel)
.constrain("front", "Fixed")
.addS(self.surface_s3(front=False), name="back",
material=self.mat_panel, role=self.role_panel)
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
param=self.s1_thickness)
)
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")
)
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()
2024-07-11 16:02:54 -07:00
2024-07-16 15:42:39 -07:00
@assembly()
def assembly(self,
parts: Optional[list[str]] = None,
shoulder_deflection: float = 0.0,
elbow_wrist_deflection: float = 0.0,
2024-07-19 15:06:57 -07:00
root_offset: int = 5,
fastener_pos: float = 0.0,
2024-07-22 15:02:26 -07:00
ignore_detail: bool = False,
) -> 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:
2024-07-22 15:02:26 -07:00
result.add(self.assembly_s0(ignore_detail=ignore_detail), name="s0")
2024-07-19 15:06:57 -07:00
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
2024-07-19 15:06:57 -07:00
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")
2024-07-17 00:30:41 -07:00
.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")
2024-07-17 00:30:41 -07:00
.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")
2024-07-22 09:49:16 -07:00
wrist_n_holes = len(self.wrist_joint.hole_pos)
if "s2" in parts and "wrist" in parts:
# Mounted backwards to bend in other direction
2024-07-22 09:49:16 -07:00
for i in range(wrist_n_holes):
(
result
.constrain(f"s2/wrist_top?conn{i}", f"wrist/parent_upper/lip?conn_{tag_bot}{i}", "Plane")
.constrain(f"s2/wrist_bot?conn{i}", f"wrist/parent_upper/lip?conn_{tag_top}{i}", "Plane")
)
if "s3" in parts:
result.add(self.assembly_s3(), name="s3")
if "s3" in parts and "wrist" in parts:
2024-07-22 09:49:16 -07:00
for i in range(wrist_n_holes):
(
result
.constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_{tag_bot}{i}", "Plane")
.constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_{tag_top}{i}", "Plane")
)
2024-07-16 12:03:51 -07:00
if len(parts) > 1:
result.solve()
2024-07-09 19:57:54 -07:00
2024-07-16 12:03:51 -07:00
return result
2024-07-16 17:18:28 -07:00
@dataclass(kw_only=True)
class WingR(WingProfile):
"""
Right side wings
"""
2024-07-18 11:08:34 -07:00
elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0)
2024-07-16 17:18:28 -07:00
elbow_height: float = 111.0
2024-07-18 11:08:34 -07:00
wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0)
2024-07-16 17:18:28 -07:00
wrist_height: float = 60.0
# Extends from the wrist to the tip of the arrow
arrow_height: float = 300
arrow_angle: float = -8
2024-07-16 17:18:28 -07:00
# Underapproximate the wrist tangent angle to leave no gaps on the blade
blade_wrist_approx_tangent_angle: float = 40.0
blade_overlap_arrow_height: float = 5.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
2024-07-16 17:18:28 -07:00
# 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
2024-07-16 17:18:28 -07:00
flip: bool = False
2024-07-16 17:18:28 -07:00
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
2024-07-16 17:18:28 -07:00
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()
2024-07-16 17:18:28 -07:00
@property
def ring_radius(self) -> float:
(dx, dy), _ = self.ring_rel_loc.to2d()
2024-07-16 17:18:28 -07:00
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(),
2024-07-16 17:18:28 -07:00
],
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(),
2024-07-16 17:18:28 -07:00
],
tag="s1_bot")
)
result = (
result
.segment(
self.wrist_bot_loc.to2d_pos(),
self.arrow_bot_loc.to2d_pos(),
2024-07-16 17:18:28 -07:00
)
.segment(
self.arrow_bot_loc.to2d_pos(),
self.arrow_other_loc.to2d_pos(),
2024-07-16 17:18:28 -07:00
)
.segment(
self.arrow_other_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
2024-07-16 17:18:28 -07:00
)
)
# Carve out the ring
result = result.assemble()
result = (
result
.push([self.ring_loc.to2d_pos()])
2024-07-16 17:18:28 -07:00
.circle(self.ring_radius, mode='a')
.circle(self.ring_radius_inner, mode='s')
.clean()
)
return result
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 + self.blade_overlap_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')
)
2024-07-17 01:19:17 -07:00
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()
2024-07-17 01:19:17 -07:00
return [
(0, -l),
(elbow_x, -l),
self.elbow_bot_loc.to2d_pos(),
self.elbow_top_loc.to2d_pos(),
(elbow_top_x, l),
2024-07-17 01:19:17 -07:00
(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()
2024-07-17 01:19:17 -07:00
return [
(0, -l),
(wrist_x, -l),
self.wrist_bot_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
2024-07-17 01:19:17 -07:00
#(self.wrist_top_x, self.wrist_top_y),
(0, wrist_top_y),
2024-07-17 01:19:17 -07:00
]
2024-07-16 17:18:28 -07:00
@dataclass(kw_only=True)
class WingL(WingProfile):
elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0)
2024-07-16 17:18:28 -07:00
elbow_height: float = 80.0
wrist_angle: float = -45.0
wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0)
2024-07-16 17:18:28 -07:00
wrist_height: float = 43.0
2024-07-18 11:08:34 -07:00
shoulder_bezier_ext: float = 120.0
shoulder_bezier_drop: float = 15.0
elbow_bezier_ext: float = 80.0
2024-07-16 17:18:28 -07:00
wrist_bezier_ext: float = 30.0
arrow_length: float = 135.0
arrow_height: float = 120.0
flip: bool = True
2024-07-22 09:49:16 -07:00
elbow_axle_pos: float = 0.4
wrist_axle_pos: float = 0.5
2024-07-16 17:18:28 -07:00
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
2024-07-18 14:09:53 -07:00
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__()
2024-07-16 17:18:28 -07:00
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()
2024-07-16 17:18:28 -07:00
def profile(self) -> Cq.Sketch:
result = (
Cq.Sketch()
.segment(
(0,0),
(0, self.shoulder_height)
)
.bezier([
(0, 0),
2024-07-18 11:08:34 -07:00
(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(),
2024-07-16 17:18:28 -07:00
])
.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(),
2024-07-16 17:18:28 -07:00
])
.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(),
2024-07-16 17:18:28 -07:00
])
.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(),
2024-07-16 17:18:28 -07:00
])
)
# 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(),
2024-07-16 17:18:28 -07:00
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(),
2024-07-16 17:18:28 -07:00
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()
2024-07-17 01:19:17 -07:00
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()
2024-07-17 01:19:17 -07:00
return [
(0, -l),
(elbow_bot_x, -l),
self.elbow_bot_loc.to2d_pos(),
self.elbow_top_loc.to2d_pos(),
(elbow_top_x, l),
2024-07-17 01:19:17 -07:00
(0, l)
]
def _mask_wrist(self) -> list[Tuple[float, float]]:
l = 200
elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos()
2024-07-21 00:17:43 -07:00
elbow_top_x, elbow_top_y = self.elbow_top_loc.to2d_pos()
_, wrist_bot_y = self.wrist_bot_loc.to2d_pos()
2024-07-21 00:17:43 -07:00
wrist_top_x, wrist_top_y = self.wrist_top_loc.to2d_pos()
2024-07-17 01:19:17 -07:00
return [
(0, -l),
(elbow_bot_x, wrist_bot_y),
self.wrist_bot_loc.to2d_pos(),
self.wrist_top_loc.to2d_pos(),
2024-07-21 00:17:43 -07:00
(wrist_top_x, wrist_top_y + l),
(elbow_top_x, elbow_top_y + l),
2024-07-17 01:19:17 -07:00
(0, l),
]