cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
5 changed files with 189 additions and 117 deletions
Showing only changes of commit ac6710eeeb - Show all commits

View File

@ -1,7 +1,7 @@
""" """
Geometry functions Geometry functions
""" """
from typing import Tuple from typing import Tuple, Optional
import math import math
def min_radius_contraction_span_pos( def min_radius_contraction_span_pos(
@ -62,3 +62,49 @@ def min_tangent_contraction_span_pos(
phi = phi_ + theta phi = phi_ + theta
assert theta <= phi < math.pi assert theta <= phi < math.pi
return r, phi, oq return r, phi, oq
def contraction_span_pos_from_radius(
d_open: float,
d_closed: float,
theta: float,
r: Optional[float] = None,
smaller: bool = True,
) -> Tuple[float, float, float]:
"""
Returns `(r, phi, r')`
Set `smaller` to false to use the other solution, which has a larger
profile.
"""
if r is None:
return min_tangent_contraction_span_pos(
d_open=d_open,
d_closed=d_closed,
theta=theta)
assert 0 < theta < math.pi
assert d_open > d_closed
assert r > 0
# Law of cosines
pp_ = r * math.sqrt(2 * (1 - math.cos(theta)))
d = d_open - d_closed
assert pp_ > d, f"Triangle inequality is violated. This joint is impossible: {pp_}, {d}"
assert d_open + d_closed > pp_, f"The span is too great to cover with this stroke length: {pp_}"
# Angle of PP'Q, via a numerically stable acos
beta = math.acos(
- d / pp_ * (1 + d / (2 * d_closed))
+ pp_ / (2 * d_closed))
# Two solutions based on angle complementarity
if smaller:
contra_phi = beta - (math.pi - theta) / 2
else:
# technically there's a 2pi in front
contra_phi = -(math.pi - theta) / 2 - beta
# Law of cosines, calculates `r'`
r_ = math.sqrt(
r * r + d_closed * d_closed - 2 * r * d_closed * math.cos(contra_phi)
)
# sin phi_ / P'Q = sin contra_phi / r'
phi_ = math.asin(math.sin(contra_phi) / r_ * d_closed)
assert phi_ > 0, f"Actuator would need to traverse pass its minimal point, {math.degrees(phi_)}"
assert 0 <= theta + phi_ <= math.pi
return r, theta + phi_, r_

View File

@ -112,6 +112,44 @@ class TestGeometry(unittest.TestCase):
y = r * math.sin(phi - theta) y = r * math.sin(phi - theta)
d = math.sqrt((x - rp) ** 2 + y ** 2) d = math.sqrt((x - rp) ** 2 + y ** 2)
self.assertAlmostEqual(d, dc) self.assertAlmostEqual(d, dc)
def test_contraction_span_pos_from_radius(self):
sl = 50.0
dc = 112.0
do = dc + sl
r = 70.0
theta = math.radians(60.0)
for smaller in [False, True]:
with self.subTest(smaller=smaller):
r, phi, rp = nhf.geometry.contraction_span_pos_from_radius(do, dc, r=r, theta=theta, smaller=smaller)
with self.subTest(state='open'):
x = r * math.cos(phi)
y = r * math.sin(phi)
d = math.sqrt((x - rp) ** 2 + y ** 2)
self.assertAlmostEqual(d, do)
with self.subTest(state='closed'):
x = r * math.cos(phi - theta)
y = r * math.sin(phi - theta)
d = math.sqrt((x - rp) ** 2 + y ** 2)
self.assertAlmostEqual(d, dc)
def test_contraction_span_pos_from_radius_2(self):
sl = 40.0
dc = 170.0
do = dc + sl
r = 50.0
theta = math.radians(120.0)
for smaller in [False, True]:
with self.subTest(smaller=smaller):
r, phi, rp = nhf.geometry.contraction_span_pos_from_radius(do, dc, r=r, theta=theta, smaller=smaller)
with self.subTest(state='open'):
x = r * math.cos(phi)
y = r * math.sin(phi)
d = math.sqrt((x - rp) ** 2 + y ** 2)
self.assertAlmostEqual(d, do)
with self.subTest(state='closed'):
x = r * math.cos(phi - theta)
y = r * math.sin(phi - theta)
d = math.sqrt((x - rp) ** 2 + y ** 2)
self.assertAlmostEqual(d, dc)
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):

View File

@ -51,7 +51,7 @@ class LinearActuator(Item):
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
def generate(self, pos: float=0) -> Cq.Assembly: def generate(self, pos: float=0) -> Cq.Assembly:
assert -1e-6 <= pos <= 1 + 1e-6 assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}"
stroke_x = pos * self.stroke_length stroke_x = pos * self.stroke_length
front = ( front = (
Cq.Workplane('XZ') Cq.Workplane('XZ')
@ -339,6 +339,7 @@ class Flexor:
Actuator assembly which flexes, similar to biceps Actuator assembly which flexes, similar to biceps
""" """
motion_span: float motion_span: float
arm_radius: Optional[float] = None
actuator: LinearActuator = LINEAR_ACTUATOR_50 actuator: LinearActuator = LINEAR_ACTUATOR_50
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
@ -352,22 +353,11 @@ class Flexor:
return self.bracket.hole_to_side_ext return self.bracket.hole_to_side_ext
def open_pos(self) -> Tuple[float, float, float]: def open_pos(self) -> Tuple[float, float, float]:
r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos( r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
d_open=self.actuator.conn_length + self.actuator.stroke_length,
d_closed=self.actuator.conn_length,
theta=math.radians(self.motion_span),
)
return r, math.degrees(phi), r_
#r, phi = nhf.geometry.min_radius_contraction_span_pos(
# d_open=self.actuator.conn_length + self.actuator.stroke_length,
# d_closed=self.actuator.conn_length,
# theta=math.radians(self.motion_span),
#)
#return r, math.degrees(phi), r
r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos(
d_open=self.actuator.conn_length + self.actuator.stroke_length, d_open=self.actuator.conn_length + self.actuator.stroke_length,
d_closed=self.actuator.conn_length, d_closed=self.actuator.conn_length,
theta=math.radians(self.motion_span), theta=math.radians(self.motion_span),
r=self.arm_radius,
) )
return r, math.degrees(phi), r_ return r, math.degrees(phi), r_
@ -378,12 +368,15 @@ class Flexor:
""" """
Length of the actuator at some angle Length of the actuator at some angle
""" """
assert 0 <= angle <= self.motion_span
r, phi, rp = self.open_pos() r, phi, rp = self.open_pos()
th = math.radians(phi - angle) th = math.radians(phi - angle)
return math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
# Law of cosines result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
d2 = r * r + rp * rp - 2 * r * rp * math.cos(th) #result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
return math.sqrt(d2) assert self.actuator.conn_length <= result <= self.actuator.conn_length + self.actuator.stroke_length, \
f"Illegal length: {result} in {self.actuator.conn_length}+{self.actuator.stroke_length}"
return result
def add_to( def add_to(

View File

@ -768,7 +768,7 @@ class DiskJoint(Model):
radius_axle: float = 3.0 radius_axle: float = 3.0
housing_thickness: float = 4.0 housing_thickness: float = 4.0
disk_thickness: float = 7.0 disk_thickness: float = 8.0
# Amount by which the wall carves in # Amount by which the wall carves in
wall_inset: float = 2.0 wall_inset: float = 2.0
@ -784,7 +784,7 @@ class DiskJoint(Model):
# leave some gap for cushion # leave some gap for cushion
movement_gap: float = 5.0 movement_gap: float = 5.0
# Angular span of tongue on disk # Angular span of tongue on disk
tongue_span: float = 30.0 tongue_span: float = 25.0
tongue_length: float = 10.0 tongue_length: float = 10.0
generate_inner_wall: bool = False generate_inner_wall: bool = False
@ -1053,7 +1053,7 @@ class ElbowJoint(Model):
# Extra bit on top of the lip to connect to actuator mount # Extra bit on top of the lip to connect to actuator mount
child_lip_extra_length: float = 1.0 child_lip_extra_length: float = 1.0
lip_length: float = 60.0 lip_length: float = 60.0
hole_pos: list[float] = field(default_factory=lambda: [15, 25]) hole_pos: list[float] = field(default_factory=lambda: [12, 24])
parent_arm_width: float = 10.0 parent_arm_width: float = 10.0
# Angle of the beginning of the parent arm # Angle of the beginning of the parent arm
parent_arm_angle: float = 180.0 parent_arm_angle: float = 180.0
@ -1074,6 +1074,7 @@ class ElbowJoint(Model):
# Rotates the surface of the mount relative to radially inwards # Rotates the surface of the mount relative to radially inwards
flexor_mount_angle_parent: float = 0 flexor_mount_angle_parent: float = 0
flexor_mount_angle_child: float = -90 flexor_mount_angle_child: float = -90
flexor_child_arm_radius: Optional[float] = None
def __post_init__(self): def __post_init__(self):
assert self.child_arm_radius > self.disk_joint.radius_housing assert self.child_arm_radius > self.disk_joint.radius_housing
@ -1082,7 +1083,8 @@ class ElbowJoint(Model):
if self.actuator: if self.actuator:
self.flexor = Flexor( self.flexor = Flexor(
actuator=self.actuator, actuator=self.actuator,
motion_span=self.motion_span motion_span=self.motion_span,
arm_radius=self.flexor_child_arm_radius,
) )
def hole_loc_tags(self): def hole_loc_tags(self):
@ -1263,18 +1265,18 @@ class ElbowJoint(Model):
#.solve() #.solve()
) )
if self.flexor: if self.flexor:
result.add(
Cq.Edge.makeLine((-1,0,0), (1,0,0)),
name="act",
loc=self.actuator_mount_loc(child=False, unflip=True))
if generate_mount: if generate_mount:
# Orientes the hole surface so it faces +X # Orientes the hole surface so it faces +X
loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90)
result.add( result.add(
self.actuator_mount(), self.actuator_mount(),
name="act", name="act_mount",
loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness) loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness
else: )
result.add(
Cq.Edge.makeLine((-1,0,0), (1,0,0)),
name="act",
loc=self.actuator_mount_loc(child=False, unflip=True))
return result return result
@assembly() @assembly()

View File

@ -22,6 +22,31 @@ from nhf.touhou.houjuu_nue.electronics import (
) )
import nhf.utils import nhf.utils
ELBOW_PARAMS = dict(
disk_joint=DiskJoint(
movement_angle=55,
),
hole_diam=4.0,
actuator=LINEAR_ACTUATOR_50,
parent_arm_width=15,
)
WRIST_PARAMS = dict(
disk_joint=DiskJoint(
movement_angle=30,
radius_disk=13.0,
radius_housing=15.0,
),
hole_pos=[10],
lip_length=30,
child_arm_radius=23.0,
parent_arm_radius=30.0,
hole_diam=4.0,
angle_neutral=0.0,
actuator=LINEAR_ACTUATOR_10,
flexor_offset_angle=30.0,
flexor_child_arm_radius=None,
)
@dataclass(kw_only=True) @dataclass(kw_only=True)
class WingProfile(Model): class WingProfile(Model):
@ -55,37 +80,11 @@ class WingProfile(Model):
s1_thickness: float = 25.0 s1_thickness: float = 25.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( elbow_joint: ElbowJoint
disk_joint=DiskJoint(
movement_angle=55,
),
hole_diam=4.0,
angle_neutral=10.0,
actuator=LINEAR_ACTUATOR_50,
flexor_offset_angle=30,
parent_arm_width=15,
child_lip_extra_length=8,
flip=False,
))
# Distance between the two spacers on the elbow, halved # Distance between the two spacers on the elbow, halved
elbow_h2: float = 5.0 elbow_h2: float = 5.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( wrist_joint: ElbowJoint
disk_joint=DiskJoint(
movement_angle=30,
radius_disk=13.0,
radius_housing=15.0,
),
hole_pos=[10],
lip_length=30,
child_arm_radius=23.0,
parent_arm_radius=30.0,
hole_diam=4.0,
angle_neutral=0.0,
actuator=LINEAR_ACTUATOR_10,
flexor_offset_angle=30.0,
flip=True,
))
# Distance between the two spacers on the elbow, halved # Distance between the two spacers on the elbow, halved
wrist_h2: float = 5.0 wrist_h2: float = 5.0
@ -99,7 +98,7 @@ class WingProfile(Model):
elbow_height: float elbow_height: float
wrist_bot_loc: Cq.Location wrist_bot_loc: Cq.Location
wrist_height: float wrist_height: float
elbow_rotate: float = 10.0 elbow_rotate: float
wrist_rotate: float = -30.0 wrist_rotate: float = -30.0
# Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side)
elbow_axle_pos: float elbow_axle_pos: float
@ -575,35 +574,18 @@ class WingProfile(Model):
Polygon shape to mask wrist Polygon shape to mask wrist
""" """
def spacer_of_joint(
self,
joint: ElbowJoint,
segment_thickness: float,
dx: float) -> MountingBox:
length = joint.lip_length / 2 - dx
holes = [
Hole(x - dx)
for x in joint.hole_pos
]
mbox = MountingBox(
length=length,
width=segment_thickness,
thickness=self.spacer_thickness,
holes=holes,
hole_diam=joint.hole_diam,
centred=(False, True),
)
return mbox
def _spacer_from_disk_joint( def _spacer_from_disk_joint(
self, self,
joint: ElbowJoint, joint: ElbowJoint,
segment_thickness: float, segment_thickness: float,
child: bool=False,
) -> MountingBox: ) -> MountingBox:
sign = 1 if child else -1
holes = [ holes = [
Hole(x, tag=tag) Hole(sign * x, tag=tag)
for x, tag in joint.hole_loc_tags() for x, tag in joint.hole_loc_tags()
] ]
# FIXME: Carve out the sides so light can pass through
mbox = MountingBox( mbox = MountingBox(
length=joint.lip_length, length=joint.lip_length,
width=segment_thickness, width=segment_thickness,
@ -789,6 +771,7 @@ class WingProfile(Model):
return self._spacer_from_disk_joint( return self._spacer_from_disk_joint(
joint=self.elbow_joint, joint=self.elbow_joint,
segment_thickness=self.s2_thickness, segment_thickness=self.s2_thickness,
child=True,
) )
@submodel(name="spacer-s2-wrist") @submodel(name="spacer-s2-wrist")
def spacer_s2_wrist(self) -> MountingBox: def spacer_s2_wrist(self) -> MountingBox:
@ -847,7 +830,7 @@ class WingProfile(Model):
result, result,
o.generate(), o.generate(),
point_tag=t, point_tag=t,
flipped=is_parent, flipped=True,#is_parent,
) )
return result.solve() return result.solve()
@ -907,6 +890,7 @@ class WingProfile(Model):
return self._spacer_from_disk_joint( return self._spacer_from_disk_joint(
joint=self.wrist_joint, joint=self.wrist_joint,
segment_thickness=self.s3_thickness, segment_thickness=self.s3_thickness,
child=True,
) )
@assembly() @assembly()
def assembly_s3(self) -> Cq.Assembly: def assembly_s3(self) -> Cq.Assembly:
@ -952,6 +936,9 @@ class WingProfile(Model):
ignore_electronics: bool = False, ignore_electronics: bool = False,
ignore_actuators: bool = False, ignore_actuators: bool = False,
) -> Cq.Assembly(): ) -> Cq.Assembly():
assert 0 <= elbow_wrist_deflection <= 1
assert 0 <= shoulder_deflection <= 1
assert 0 <= fastener_pos <= 1
if parts is None: if parts is None:
parts = [ parts = [
"root", "root",
@ -963,9 +950,7 @@ class WingProfile(Model):
"wrist", "wrist",
"s3", "s3",
] ]
result = ( result = Cq.Assembly()
Cq.Assembly()
)
tag_top, tag_bot = "top", "bot" tag_top, tag_bot = "top", "bot"
if self.flip: if self.flip:
tag_top, tag_bot = tag_bot, tag_top tag_top, tag_bot = tag_bot, tag_top
@ -1019,11 +1004,11 @@ class WingProfile(Model):
result.constrain( result.constrain(
f"s1/elbow?{tag}", f"s1/elbow?{tag}",
f"elbow/parent_upper/lip?{tag}", "Plane") f"elbow/parent_upper/lip?{tag}", "Plane")
if not ignore_actuators: #if not ignore_actuators:
result.constrain( # result.constrain(
"elbow/bracket_back?conn_side", # "elbow/bracket_back?conn_side",
"s1/elbow_act?conn0", # "s1/elbow_act?conn0",
"Plane") # "Plane")
if "s2" in parts: if "s2" in parts:
result.add(self.assembly_s2(), name="s2") result.add(self.assembly_s2(), name="s2")
if "s2" in parts and "elbow" in parts: if "s2" in parts and "elbow" in parts:
@ -1048,11 +1033,11 @@ class WingProfile(Model):
result.constrain( result.constrain(
f"s3/wrist?{tag}", f"s3/wrist?{tag}",
f"wrist/child/lip?{tag}", "Plane") f"wrist/child/lip?{tag}", "Plane")
if not ignore_actuators: #if not ignore_actuators:
result.constrain( # result.constrain(
"wrist/bracket_back?conn_side", # "wrist/bracket_back?conn_side",
"s2/wrist_act?conn0", # "s2/wrist_act?conn0",
"Plane") # "Plane")
if len(parts) > 1: if len(parts) > 1:
result.solve() result.solve()
@ -1068,9 +1053,23 @@ class WingR(WingProfile):
elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0) elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0)
elbow_height: float = 111.0 elbow_height: float = 111.0
elbow_rotate: float = 10.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
flexor_offset_angle=15,
flexor_mount_angle_child=-75,
flexor_child_arm_radius=None,
angle_neutral=10.0,
child_lip_extra_length=8,
flip=False,
**ELBOW_PARAMS
))
wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0) wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0)
wrist_height: float = 60.0 wrist_height: float = 60.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
flip=True,
**WRIST_PARAMS
))
# Extends from the wrist to the tip of the arrow # Extends from the wrist to the tip of the arrow
arrow_height: float = 300 arrow_height: float = 300
@ -1104,8 +1103,6 @@ class WingR(WingProfile):
* Cq.Location.rot2d(self.arrow_angle) \ * Cq.Location.rot2d(self.arrow_angle) \
* Cq.Location.from2d(0, self.arrow_height + self.wrist_height) * Cq.Location.from2d(0, self.arrow_height + self.wrist_height)
self.ring_loc = self.wrist_top_loc * self.ring_rel_loc self.ring_loc = self.wrist_top_loc * self.ring_rel_loc
self.elbow_joint.flexor_offset_angle = 15
self.elbow_joint.flexor_mount_angle_child = -75
assert self.ring_radius > self.ring_radius_inner assert self.ring_radius > self.ring_radius_inner
assert 0 > self.blade_overlap_angle > self.arrow_angle assert 0 > self.blade_overlap_angle > self.arrow_angle
@ -1216,26 +1213,12 @@ class WingR(WingProfile):
This extension profile is required to accomodate the awkward shaped This extension profile is required to accomodate the awkward shaped
joint next to the scythe joint next to the scythe
""" """
# Generates the extension profile, which is required on both sides
profile = self._child_joint_extension_profile( profile = self._child_joint_extension_profile(
axle_loc=self.wrist_axle_loc, axle_loc=self.wrist_axle_loc,
radius=self.wrist_height, radius=self.wrist_height,
angle_span=self.wrist_joint.motion_span, angle_span=self.wrist_joint.motion_span,
bot=self.flip, bot=False,
) )
# 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 return profile
def profile_s3_extra(self) -> Cq.Sketch: def profile_s3_extra(self) -> Cq.Sketch:
@ -1274,7 +1257,6 @@ class WingR(WingProfile):
.circle(self.blade_hole_diam / 2, mode='s') .circle(self.blade_hole_diam / 2, mode='s')
) )
def _mask_elbow(self) -> list[Tuple[float, float]]: def _mask_elbow(self) -> list[Tuple[float, float]]:
l = 200 l = 200
elbow_x, _ = self.elbow_bot_loc.to2d_pos() elbow_x, _ = self.elbow_bot_loc.to2d_pos()
@ -1306,11 +1288,24 @@ class WingR(WingProfile):
class WingL(WingProfile): class WingL(WingProfile):
elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0) elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0)
elbow_height: float = 80.0 elbow_height: float = 90.0
elbow_rotate: float = 15.0
elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
angle_neutral=30.0,
flexor_mount_angle_child=180,
flexor_offset_angle=15,
flexor_child_arm_radius=60.0,
flip=True,
**ELBOW_PARAMS
))
wrist_angle: float = -45.0 wrist_angle: float = -45.0
wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0)
wrist_height: float = 43.0 wrist_height: float = 43.0
wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint(
flip=False,
**WRIST_PARAMS
))
shoulder_bezier_ext: float = 120.0 shoulder_bezier_ext: float = 120.0
shoulder_bezier_drop: float = 15.0 shoulder_bezier_drop: float = 15.0
@ -1326,15 +1321,13 @@ class WingL(WingProfile):
elbow_joint_overlap_median: float = 0.5 elbow_joint_overlap_median: float = 0.5
wrist_joint_overlap_median: float = 0.5 wrist_joint_overlap_median: float = 0.5
def __post_init__(self): def __post_init__(self):
assert self.wrist_height <= self.shoulder_joint.height assert self.wrist_height <= self.shoulder_joint.height
self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle)
self.elbow_joint.angle_neutral = 15.0
self.elbow_joint.flip = True
self.elbow_rotate = 5.0
self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0
self.wrist_rotate = -self.wrist_joint.angle_neutral self.wrist_rotate = -self.wrist_joint.angle_neutral
self.wrist_joint.flip = False
self.shoulder_joint.flip = True self.shoulder_joint.flip = True
super().__post_init__() super().__post_init__()