1673 lines
60 KiB
Python
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),
|
|
]
|