cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
6 changed files with 155 additions and 16 deletions
Showing only changes of commit 2bdae6df01 - Show all commits

View File

@ -50,6 +50,8 @@ class MountingBox(Model):
centred: Tuple[bool, bool] = (False, True)
generate_side_tags: bool = True
# Generate tags on the opposite side
generate_reverse_tags: bool = False
# Determines the position of side tags
flip_y: bool = False
@ -81,9 +83,13 @@ class MountingBox(Model):
.extrude(self.thickness)
)
plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness)
reverse_plane = result.copyWorkplane(Cq.Workplane('XY'))
for i, hole in enumerate(self.holes):
tag = hole.tag if hole.tag else f"conn{i}"
plane.moveTo(hole.x, hole.y).tagPlane(tag)
if self.generate_reverse_tags:
rev_tag = hole.tag + "_rev" if hole.tag else f"conn{i}_rev"
reverse_plane.moveTo(hole.x, hole.y).tagPlane(rev_tag, '-Z')
if self.generate_side_tags:
result.faces("<Y").workplane(origin=result.vertices("<X and <Y and >Z").val().Center()).tagPlane("left")
@ -102,6 +108,8 @@ class MountingBox(Model):
)
for i in range(len(self.holes)):
result.markPlane(f"box?conn{i}")
if self.generate_reverse_tags:
result.markPlane(f"box?conn{i}_rev")
if self.generate_side_tags:
(
result

View File

@ -2,12 +2,14 @@
Electronic components
"""
from dataclasses import dataclass
from typing import Optional
from typing import Optional, Tuple
import math
import cadquery as Cq
from nhf.materials import Role
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
import nhf.utils
import scipy.optimize as SO
@dataclass(frozen=True)
class LinearActuator(Item):
@ -47,6 +49,7 @@ class LinearActuator(Item):
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
def generate(self, pos: float=0) -> Cq.Assembly:
assert -1e-6 <= pos <= 1 + 1e-6
stroke_x = pos * self.stroke_length
front = (
Cq.Workplane('XZ')
@ -204,10 +207,37 @@ class MountingBracket(Item):
return result
LINEAR_ACTUATOR_SHOULDER = LinearActuator(
LINEAR_ACTUATOR_50 = LinearActuator(
mass=34.0,
stroke_length=50,
# FIXME: Measure
front_hole_ext=6,
back_hole_ext=6,
segment1_length=50,
segment2_length=50,
)
LINEAR_ACTUATOR_30 = LinearActuator(
mass=34.0,
stroke_length=30,
)
LINEAR_ACTUATOR_21 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=21,
front_hole_ext=4,
back_hole_ext=4,
segment1_length=75/2,
segment2_length=75/2,
)
LINEAR_ACTUATOR_10 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=10,
front_hole_ext=4.5/2,
back_hole_ext=4.5/2,
segment1_length=30.0,
segment2_length=30.0,
)
LINEAR_ACTUATOR_HEX_NUT = HexNut(
mass=0.8,
diam_thread=4,
@ -224,18 +254,61 @@ LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
)
LINEAR_ACTUATOR_BRACKET = MountingBracket()
@dataclass(frozen=True)
class LinearActuatorAssembly:
@dataclass
class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
# FIXME: Measure
actuator: LinearActuator = LINEAR_ACTUATOR_SHOULDER
actuator: LinearActuator = LINEAR_ACTUATOR_30
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
# FIXME: Add a compression spring so the serviceable distances are not as fixed
mount_loc_r: float = float('nan')
mount_loc_angle: float = float('nan')
def __post_init__(self):
d_open = self.actuator.conn_length + self.actuator.stroke_length
d_closed = self.actuator.conn_length
theta = math.radians(self.motion_span)
def target(args):
r, phi = args
e1 = d_open * d_open - 2 * r * r * (1 - math.cos(theta + phi))
e2 = d_closed * d_closed - 2 * r * r * (1 - math.cos(phi))
return [e1, e2]
self.mount_loc_r, phi = SO.fsolve(target, [self.actuator.conn_length, theta])
self.mount_loc_angle = math.degrees(theta + phi)
@property
def mount_height(self):
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 target_length_at_angle(
self,
angle: float = 0.0
) -> float:
# law of cosines
r = self.mount_loc_r
th = math.radians(self.mount_loc_angle - angle)
d2 = 2 * r * r * (1 - math.cos(th))
return math.sqrt(d2)
def add_to(
self,
a: Cq.Assembly,
target_length: float,
tag_prefix: Optional[str] = None,
tag_hole_front: Optional[str] = None,
tag_hole_back: Optional[str] = None,
@ -244,6 +317,7 @@ class LinearActuatorAssembly:
Adds the necessary mechanical components to this assembly. Does not
invoke `a.solve()`.
"""
pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length
if tag_prefix:
tag_prefix = tag_prefix + "_"
name_actuator = f"{tag_prefix}actuator"
@ -255,7 +329,7 @@ class LinearActuatorAssembly:
name_nut_back = f"{tag_prefix}back_nut"
(
a
.add(self.actuator.assembly(), name=name_actuator)
.add(self.actuator.assembly(pos=pos), name=name_actuator)
.add(self.bracket.assembly(), name=name_bracket_front)
.add(self.bolt.assembly(), name=name_bolt_front)
.add(self.nut.assembly(), name=name_nut_front)

View File

@ -8,6 +8,7 @@ from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
from nhf.touhou.houjuu_nue.electronics import Flexor
import nhf.utils
TOL = 1e-6
@ -878,10 +879,15 @@ class ElbowJoint(Model):
angle_neutral: float = 30.0
flexor: Flexor = None
flexor_offset_angle: float = 30.0
flexor_mount_rot: float = 95.0
def __post_init__(self):
assert self.child_arm_radius > self.disk_joint.radius_housing
assert self.parent_arm_radius > self.disk_joint.radius_housing
self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2
self.flexor = Flexor(motion_span=self.motion_span)
@property
def total_thickness(self):
@ -897,15 +903,40 @@ class ElbowJoint(Model):
axle is at position 0, and parent direction is -X
"""
return Cq.Location.from2d(-self.parent_arm_radius, 0, 0)
def child_arm_loc(self, flip: bool = False) -> Cq.Location:
def child_arm_loc(self, flip: bool = False, angle: float = 0.0) -> Cq.Location:
"""
2d Location of the centre of the arm surface on the child side, assuming
axle is at position 0, and parent direction is -X
Set `flip=True` to indicate that the joint is supposed to be installed upside down
"""
result = Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180)
result = Cq.Location.rot2d(self.angle_neutral + angle) * Cq.Location.from2d(self.child_arm_radius, 0, 180)
return result.flip_y() if flip else result
def actuator_mount(self) -> Cq.Workplane:
holes = [
Hole(x=0, y=0, tag="mount"),
]
mbox = MountingBox(
length=self.disk_joint.total_thickness,
width=self.disk_joint.total_thickness,
thickness=self.lip_thickness,
holes=holes,
hole_diam=self.hole_diam,
centred=(True, True),
generate_side_tags=False,
)
return mbox.generate()
def actuator_mount_loc(self, child: bool) -> Cq.Location:
# Orientes the hole surface so it faces +X
loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90)
# Moves the hole so the axle of the mount is perpendicular to it
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))
# Moves the hole to be some distance apart from 0
loc_span = Cq.Location.from2d(self.flexor.mount_loc_r, 0)
r = 0 if child else self.flexor.mount_loc_angle
loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle)
return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness
def lip(self) -> Cq.Workplane:
holes = [
@ -924,6 +955,7 @@ class ElbowJoint(Model):
hole_diam=self.hole_diam,
centred=(True, True),
generate_side_tags=False,
generate_reverse_tags=True,
)
return mbox.generate()
@ -946,8 +978,9 @@ class ElbowJoint(Model):
loc_lip.inverse * loc_cut_rel * loc_disk)
result = (
Cq.Assembly()
.add(self.lip().cut(disk_cut), name="lip", loc=loc_lip)
.add(self.disk_joint.disk(), name="disk", loc=loc_disk)
.add(self.disk_joint.disk(), name="disk")
.add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip)
.add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True))
)
return result
@ -986,16 +1019,19 @@ class ElbowJoint(Model):
-self.disk_joint.tongue_span / 2 + self.angle_neutral
)
lip_dz = self.lip_thickness
loc_net_housing = axial_offset * housing_loc
result = (
Cq.Assembly()
.add(housing, name="housing")
.add(self.lip(), name="lip", loc=
loc_net_housing.inverse *
Cq.Location((0, 0, 0), (0, 1, 0), 180) *
Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) *
Cq.Location((0, 0, 0), (0, 1, 0), 90))
.add(housing, name="housing",
loc=axial_offset * housing_loc)
.add(connector, name="connector",
loc=axial_offset)
loc=loc_net_housing.inverse * axial_offset)
.add(self.actuator_mount(),
name="act", loc=self.actuator_mount_loc(child=False))
#.constrain("housing", "Fixed")
#.constrain("connector", "Fixed")
#.solve()
@ -1021,6 +1057,17 @@ class ElbowJoint(Model):
disk="child/disk",
angle=angle,
)
if self.flexor:
target_length = self.flexor.target_length_at_angle(
angle=angle,
)
self.flexor.add_to(
result,
target_length=target_length,
tag_hole_back="parent_upper/act?mount",
tag_hole_front="child/act?mount",
tag_dir="parent_lower?mate",
)
return result.solve()
if __name__ == '__main__':

View File

@ -795,7 +795,16 @@ class WingProfile(Model):
fastener_pos: float = 0.0,
) -> Cq.Assembly():
if parts is None:
parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
parts = [
"root",
"s0",
"shoulder",
"s1",
"elbow",
"s2",
"wrist",
"s3",
]
result = (
Cq.Assembly()
)

2
poetry.lock generated
View File

@ -842,4 +842,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "3403086281e26faefd12217e6dec4c0696e3468c5a9d8c952f8d988857aafba0"
content-hash = "6fc2644e7778ba22f8f5f2bcb2ca54f03b325f62c8a3fcd1c265a17561d874b8"

View File

@ -14,6 +14,7 @@ colorama = "^0.4.6"
# cadquery dependency
multimethod = "^1.12"
scipy = "^1.14.0"
[build-system]
requires = ["poetry-core"]