cosplay: Touhou/Houjuu Nue #4
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -842,4 +842,4 @@ files = [
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "3403086281e26faefd12217e6dec4c0696e3468c5a9d8c952f8d988857aafba0"
|
||||
content-hash = "6fc2644e7778ba22f8f5f2bcb2ca54f03b325f62c8a3fcd1c265a17561d874b8"
|
||||
|
|
|
@ -14,6 +14,7 @@ colorama = "^0.4.6"
|
|||
|
||||
# cadquery dependency
|
||||
multimethod = "^1.12"
|
||||
scipy = "^1.14.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
Loading…
Reference in New Issue