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

View File

@ -2,12 +2,14 @@
Electronic components Electronic components
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional, Tuple
import math
import cadquery as Cq import cadquery as Cq
from nhf.materials import Role from nhf.materials import Role
from nhf.parts.item import Item from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut from nhf.parts.fasteners import FlatHeadBolt, HexNut
import nhf.utils import nhf.utils
import scipy.optimize as SO
@dataclass(frozen=True) @dataclass(frozen=True)
class LinearActuator(Item): 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 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
stroke_x = pos * self.stroke_length stroke_x = pos * self.stroke_length
front = ( front = (
Cq.Workplane('XZ') Cq.Workplane('XZ')
@ -204,10 +207,37 @@ class MountingBracket(Item):
return result 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, mass=34.0,
stroke_length=30, 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( LINEAR_ACTUATOR_HEX_NUT = HexNut(
mass=0.8, mass=0.8,
diam_thread=4, diam_thread=4,
@ -224,18 +254,61 @@ LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
) )
LINEAR_ACTUATOR_BRACKET = MountingBracket() LINEAR_ACTUATOR_BRACKET = MountingBracket()
@dataclass(frozen=True) @dataclass
class LinearActuatorAssembly: class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
# FIXME: Measure actuator: LinearActuator = LINEAR_ACTUATOR_30
actuator: LinearActuator = LINEAR_ACTUATOR_SHOULDER
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET 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( def add_to(
self, self,
a: Cq.Assembly, a: Cq.Assembly,
target_length: float,
tag_prefix: Optional[str] = None, tag_prefix: Optional[str] = None,
tag_hole_front: Optional[str] = None, tag_hole_front: Optional[str] = None,
tag_hole_back: 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 Adds the necessary mechanical components to this assembly. Does not
invoke `a.solve()`. invoke `a.solve()`.
""" """
pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length
if tag_prefix: if tag_prefix:
tag_prefix = tag_prefix + "_" tag_prefix = tag_prefix + "_"
name_actuator = f"{tag_prefix}actuator" name_actuator = f"{tag_prefix}actuator"
@ -255,7 +329,7 @@ class LinearActuatorAssembly:
name_nut_back = f"{tag_prefix}back_nut" name_nut_back = f"{tag_prefix}back_nut"
( (
a 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.bracket.assembly(), name=name_bracket_front)
.add(self.bolt.assembly(), name=name_bolt_front) .add(self.bolt.assembly(), name=name_bolt_front)
.add(self.nut.assembly(), name=name_nut_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.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.joints import TorsionJoint, HirthJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
from nhf.touhou.houjuu_nue.electronics import Flexor
import nhf.utils import nhf.utils
TOL = 1e-6 TOL = 1e-6
@ -878,10 +879,15 @@ class ElbowJoint(Model):
angle_neutral: float = 30.0 angle_neutral: float = 30.0
flexor: Flexor = None
flexor_offset_angle: float = 30.0
flexor_mount_rot: float = 95.0
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
assert self.parent_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.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 @property
def total_thickness(self): def total_thickness(self):
@ -897,15 +903,40 @@ class ElbowJoint(Model):
axle is at position 0, and parent direction is -X axle is at position 0, and parent direction is -X
""" """
return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) 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 2d Location of the centre of the arm surface on the child side, assuming
axle is at position 0, and parent direction is -X 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 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 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: def lip(self) -> Cq.Workplane:
holes = [ holes = [
@ -924,6 +955,7 @@ class ElbowJoint(Model):
hole_diam=self.hole_diam, hole_diam=self.hole_diam,
centred=(True, True), centred=(True, True),
generate_side_tags=False, generate_side_tags=False,
generate_reverse_tags=True,
) )
return mbox.generate() return mbox.generate()
@ -946,8 +978,9 @@ class ElbowJoint(Model):
loc_lip.inverse * loc_cut_rel * loc_disk) loc_lip.inverse * loc_cut_rel * loc_disk)
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(self.lip().cut(disk_cut), name="lip", loc=loc_lip) .add(self.disk_joint.disk(), name="disk")
.add(self.disk_joint.disk(), name="disk", loc=loc_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 return result
@ -986,16 +1019,19 @@ class ElbowJoint(Model):
-self.disk_joint.tongue_span / 2 + self.angle_neutral -self.disk_joint.tongue_span / 2 + self.angle_neutral
) )
lip_dz = self.lip_thickness lip_dz = self.lip_thickness
loc_net_housing = axial_offset * housing_loc
result = ( result = (
Cq.Assembly() Cq.Assembly()
.add(housing, name="housing")
.add(self.lip(), name="lip", loc= .add(self.lip(), name="lip", loc=
loc_net_housing.inverse *
Cq.Location((0, 0, 0), (0, 1, 0), 180) * Cq.Location((0, 0, 0), (0, 1, 0), 180) *
Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) *
Cq.Location((0, 0, 0), (0, 1, 0), 90)) Cq.Location((0, 0, 0), (0, 1, 0), 90))
.add(housing, name="housing",
loc=axial_offset * housing_loc)
.add(connector, name="connector", .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("housing", "Fixed")
#.constrain("connector", "Fixed") #.constrain("connector", "Fixed")
#.solve() #.solve()
@ -1021,6 +1057,17 @@ class ElbowJoint(Model):
disk="child/disk", disk="child/disk",
angle=angle, 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() return result.solve()
if __name__ == '__main__': if __name__ == '__main__':

View File

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

2
poetry.lock generated
View File

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

View File

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