cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
6 changed files with 98 additions and 23 deletions
Showing only changes of commit 07fb43cd01 - Show all commits

View File

@ -4,7 +4,7 @@ Geometry functions
from typing import Tuple from typing import Tuple
import math import math
def contraction_actuator_span_pos( def min_radius_contraction_span_pos(
d_open: float, d_open: float,
d_closed: float, d_closed: float,
theta: float, theta: float,
@ -20,6 +20,9 @@ def contraction_actuator_span_pos(
fully open (resp. closed), `Q` be the position of the back of the actuator, fully open (resp. closed), `Q` be the position of the back of the actuator,
we note that `OP = OP' = OQ`. we note that `OP = OP' = OQ`.
""" """
assert d_open > d_closed
assert 0 < theta < math.pi
pq2 = d_open * d_open pq2 = d_open * d_open
p_q2 = d_closed * d_closed p_q2 = d_closed * d_closed
# angle of PQP' # angle of PQP'
@ -30,3 +33,32 @@ def contraction_actuator_span_pos(
# Law of cosines on POQ: # Law of cosines on POQ:
phi = math.acos(1 - pq2 / 2 / r2) phi = math.acos(1 - pq2 / 2 / r2)
return math.sqrt(r2), phi return math.sqrt(r2), phi
def min_tangent_contraction_span_pos(
d_open: float,
d_closed: float,
theta: float,
) -> Tuple[float, float, float]:
"""
Returns `(r, phi, r')` where `r` is the distance of the arm to origin, `r'`
is the distance of the base to origin, and `phi` the angle in the open
state.
"""
assert d_open > d_closed
assert 0 < theta < math.pi
# Angle of OPQ = OPP'
pp_ = d_open - d_closed
pq = d_open
p_q = d_closed
a = (math.pi - theta) / 2
# Law of sines on POP'
r = math.sin(a) / math.sin(theta) * pp_
# Law of cosine on OPQ
oq = math.sqrt(r * r + pq * pq - 2 * r * pq * math.cos(a))
# Law of sines on OP'Q. Not using OPQ for numerical reasons since the angle
# `phi` could be very close to `pi/2`
phi_ = math.asin(math.sin(a) / oq * p_q)
phi = phi_ + theta
assert theta <= phi < math.pi
return r, phi, oq

View File

@ -80,11 +80,12 @@ class TestChecks(unittest.TestCase):
class TestGeometry(unittest.TestCase): class TestGeometry(unittest.TestCase):
def test_actuator_arm_pos(self): def test_min_radius_contraction_span_pos(self):
do = 70.0 sl = 50.0
dc = 40.0 dc = 112.0
theta = math.radians(35.0) do = dc + sl
r, phi = nhf.geometry.contraction_actuator_span_pos(do, dc, theta) theta = math.radians(60.0)
r, phi = nhf.geometry.min_radius_contraction_span_pos(do, dc, theta)
with self.subTest(state='open'): with self.subTest(state='open'):
x = r * math.cos(phi) x = r * math.cos(phi)
y = r * math.sin(phi) y = r * math.sin(phi)
@ -95,6 +96,22 @@ class TestGeometry(unittest.TestCase):
y = r * math.sin(phi - theta) y = r * math.sin(phi - theta)
d = math.sqrt((x - r) ** 2 + y ** 2) d = math.sqrt((x - r) ** 2 + y ** 2)
self.assertAlmostEqual(d, dc) self.assertAlmostEqual(d, dc)
def test_min_tangent_contraction_span_pos(self):
sl = 50.0
dc = 112.0
do = dc + sl
theta = math.radians(60.0)
r, phi, rp = nhf.geometry.min_tangent_contraction_span_pos(do, dc, theta)
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

@ -338,29 +338,39 @@ class Flexor:
@property @property
def mount_height(self): def mount_height(self):
return self.bracket.hole_to_side_ext return self.bracket.hole_to_side_ext
@property
def min_serviceable_distance(self):
return self.bracket.hole_to_side_ext * 2 + self.actuator.conn_length
@property
def max_serviceable_distance(self):
return self.min_serviceable_distance + self.actuator.stroke_length
def open_pos(self) -> Tuple[float, float]: def open_pos(self) -> Tuple[float, float, float]:
r, phi = nhf.geometry.contraction_actuator_span_pos( 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),
) )
return r, math.degrees(phi) 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_closed=self.actuator.conn_length,
theta=math.radians(self.motion_span),
)
return r, math.degrees(phi), r_
def target_length_at_angle( def target_length_at_angle(
self, self,
angle: float = 0.0 angle: float = 0.0
) -> float: ) -> float:
# law of cosines """
r, phi = self.open_pos() Length of the actuator at some angle
"""
r, phi, rp = self.open_pos()
th = math.radians(phi - angle) th = math.radians(phi - angle)
d2 = 2 * r * r * (1 - math.cos(th)) return math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
# Law of cosines
d2 = r * r + rp * rp - 2 * r * rp * math.cos(th)
return math.sqrt(d2) return math.sqrt(d2)

View File

@ -943,8 +943,8 @@ class ElbowJoint(Model):
loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180)
loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1))
# Moves the hole to be some distance apart from 0 # Moves the hole to be some distance apart from 0
mount_r, mount_loc_angle = self.flexor.open_pos() mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos()
loc_span = Cq.Location.from2d(mount_r, 0) loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0)
r = (-mount_loc_angle if child else 0) + 180 r = (-mount_loc_angle if child else 0) + 180
loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle)
return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness
@ -1055,6 +1055,7 @@ class ElbowJoint(Model):
@assembly() @assembly()
def assembly(self, angle: float = 0) -> Cq.Assembly: def assembly(self, angle: float = 0) -> Cq.Assembly:
assert 0 <= angle <= self.motion_span
result = ( result = (
Cq.Assembly() Cq.Assembly()
.addS(self.child_joint(), name="child", .addS(self.child_joint(), name="child",

View File

@ -2,8 +2,23 @@ import unittest
import cadquery as Cq import cadquery as Cq
import nhf.touhou.houjuu_nue as M import nhf.touhou.houjuu_nue as M
import nhf.touhou.houjuu_nue.joints as MJ import nhf.touhou.houjuu_nue.joints as MJ
import nhf.touhou.houjuu_nue.electronics as ME
from nhf.checks import pairwise_intersection from nhf.checks import pairwise_intersection
class TestElectronics(unittest.TestCase):
def test_flexor(self):
flexor = ME.Flexor(
motion_span=60,
)
self.assertAlmostEqual(
flexor.target_length_at_angle(0),
flexor.actuator.stroke_length + flexor.actuator.conn_length)
self.assertAlmostEqual(
flexor.target_length_at_angle(flexor.motion_span),
flexor.actuator.conn_length)
class TestJoints(unittest.TestCase): class TestJoints(unittest.TestCase):
def test_shoulder_collision_of_torsion_joint(self): def test_shoulder_collision_of_torsion_joint(self):

View File

@ -6,6 +6,7 @@ import math
from enum import Enum from enum import Enum
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Mapping, Tuple, Optional from typing import Mapping, Tuple, Optional
import cadquery as Cq import cadquery as Cq
from nhf import Material, Role from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.build import Model, TargetKind, target, assembly, submodel
@ -71,8 +72,7 @@ class WingProfile(Model):
parent_arm_radius=30.0, parent_arm_radius=30.0,
hole_diam=4.0, hole_diam=4.0,
angle_neutral=-30.0, angle_neutral=-30.0,
# The left side wrist is too small for an actuator to work actuator=LINEAR_ACTUATOR_10,
actuator=None, #LINEAR_ACTUATOR_10,
)) ))
# 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