Cosplay/nhf/touhou/houjuu_nue/joints.py

1243 lines
44 KiB
Python
Raw Normal View History

import math
2024-07-11 22:29:05 -07:00
from dataclasses import dataclass, field
2024-07-11 08:42:13 -07:00
from typing import Optional, Tuple
2024-07-10 16:21:11 -07:00
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.springs import TorsionSpring
2024-07-19 15:06:57 -07:00
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.joints import TorsionJoint, HirthJoint
2024-07-17 00:30:41 -07:00
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator
import nhf.geometry
2024-07-10 16:21:11 -07:00
import nhf.utils
TOL = 1e-6
2024-07-19 21:00:10 -07:00
# Parts used
# uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP
HS_JOINT_KNOB = ThreaddedKnob(
2024-07-20 23:11:42 -07:00
mass=77.3,
2024-07-19 21:00:10 -07:00
diam_thread=12.0,
height_thread=30.0,
diam_knob=50.0,
2024-07-20 23:11:42 -07:00
diam_neck=25.0,
height_neck=12.5,
height_knob=15.0,
2024-07-19 21:00:10 -07:00
)
# Tom's world 8Pcs M12-1.75 Hex Nut Assortment Set Stainless Steel 304(18-8)
# Metric Hexagon Nut for Bolts, Bright Finish, Full Thread (M12)
HS_JOINT_HEX_NUT = HexNut(
mass=14.9,
diam_thread=12.0,
pitch=1.75,
thickness=9.7,
width=18.9,
)
SHOULDER_AXIS_BOLT = FlatHeadBolt(
# FIXME: measure
mass=0.0,
2024-07-19 21:00:10 -07:00
diam_head=10.0,
height_head=3.0,
diam_thread=6.0,
height_thread=20.0,
)
# Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional
# Springs Repair Maintenance Spring
SHOULDER_TORSION_SPRING = TorsionSpring(
mass=2.2,
# inner diameter = 9
radius=9/2 + 1.2,
thickness=1.3,
height=7.5,
)
# KALIONE 10 Pieces Torsion Spring, Stainless Steel Small Torsion Springs, Tiny
# Torsional Spring, 90° Deflection Compression Spring Kit for Repair Tools
# Woodworking DIY, 50mm
ELBOW_TORSION_SPRING = TorsionSpring(
mass=1.7,
radius=9 / 2,
thickness=1.3,
height=6.5,
tail_length=45.0,
right_handed=False,
)
2024-07-19 15:06:57 -07:00
@dataclass
class RootJoint(Model):
"""
The Houjuu-Scarlett Mechanism
"""
2024-07-19 21:00:10 -07:00
knob: ThreaddedKnob = HS_JOINT_KNOB
hex_nut: HexNut = HS_JOINT_HEX_NUT
2024-07-19 15:06:57 -07:00
hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint(
radius=25.0,
radius_inner=15.0,
tooth_height=7.0,
base_height=5.0,
n_tooth=24,
))
parent_width: float = 85
parent_thickness: float = 10
parent_corner_fillet: float = 5
parent_corner_cbore_diam: float = 12
parent_corner_cbore_depth: float = 2
parent_corner_inset: float = 12
parent_mount_thickness: float = 25.4 / 16
child_corner_dx: float = 17.0
child_corner_dz: float = 24.0
axis_diam: float = 12.0
axis_cbore_diam: float = 20
axis_cbore_depth: float = 3
corner_hole_diam: float = 6.0
child_height: float = 60.0
child_width: float = 50.0
child_mount_thickness: float = 25.4 / 4
2024-07-19 15:06:57 -07:00
def corner_pos(self) -> list[tuple[int, int]]:
"""
Generates a set of points corresponding to the connectorss
"""
dx = self.parent_width / 2 - self.parent_corner_inset
return [
(dx, dx),
(dx, -dx),
(-dx, -dx),
(-dx, dx),
]
@property
def total_height(self) -> float:
return self.parent_thickness + self.hirth_joint.total_height
@target(name="parent")
def parent(self):
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
coupling, a cylindrical base, and a mounting base.
"""
hirth = self.hirth_joint.generate()
conn = self.corner_pos()
result = (
Cq.Workplane('XY')
.box(
self.parent_width,
self.parent_width,
self.parent_thickness,
centered=(True, True, False))
.translate((0, 0, -self.parent_thickness))
.edges("|Z")
.fillet(self.parent_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.cboreHole(
diameter=self.corner_hole_diam,
cboreDiameter=self.parent_corner_cbore_diam,
cboreDepth=self.parent_corner_cbore_depth)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.parent_thickness)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPoint(f"h{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(diameter=self.axis_diam)
.cut(self.hex_nut.generate().translate((0, 0, -self.parent_thickness)))
)
result.faces("<Z").tag("base")
return result
@target(name="child")
def child(self) -> Cq.Workplane:
hirth = self.hirth_joint.generate(is_mated=True)
dy = self.child_corner_dx
dx = self.child_corner_dz
conn = [
(-dx, -dy),
(dx, -dy),
(dx, dy),
(-dx, dy),
]
result = (
Cq.Workplane('XY')
.box(
self.child_height,
self.child_width,
self.hirth_joint.base_height,
centered=(True, True, False))
#.translate((0, 0, -self.base_joint.base_height))
#.edges("|Z")
#.fillet(self.hs_joint_corner_fillet)
.faces(">Z")
.workplane()
.pushPoints(conn)
.hole(self.corner_hole_diam)
)
# Creates a plane parallel to the holes but shifted to the base
plane = result.faces(">Z").workplane(offset=-self.hirth_joint.base_height)
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"conn{i}")
result = (
result
.faces(">Z")
.workplane()
.union(hirth, tol=0.1)
.clean()
)
result = (
result.faces("<Z")
.workplane()
.hole(self.axis_diam)
)
return result
def assembly(self,
offset: int = 0,
2024-07-23 22:40:49 -07:00
fastener_pos: float = 0,
ignore_fasteners: bool = False) -> Cq.Assembly:
2024-07-19 15:06:57 -07:00
"""
Specify knob position to determine the position of the knob from fully
inserted (0) or fully uninserted (1)
"""
2024-07-19 16:13:33 -07:00
knob_h = self.hex_nut.thickness
2024-07-19 15:06:57 -07:00
result = (
Cq.Assembly()
.addS(self.parent(), name="parent",
material=Material.PLASTIC_PLA,
role=Role.PARENT)
.constrain("parent", "Fixed")
.addS(self.child(), name="child",
material=Material.PLASTIC_PLA,
role=Role.CHILD)
)
2024-07-23 22:40:49 -07:00
if not ignore_fasteners:
(
result
.addS(self.hex_nut.assembly(), name="hex_nut")
.addS(self.knob.assembly(), name="knob",
loc=Cq.Location((0, 0, knob_h * fastener_pos)))
.constrain("knob/thread", "Fixed")
.constrain("hex_nut?bot", "parent?base", "Plane", param=0)
.constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0)
)
2024-07-19 15:06:57 -07:00
self.hirth_joint.add_constraints(
result,
"parent",
"child",
offset=offset
)
return result.solve()
@dataclass
class ShoulderJoint(Model):
2024-07-19 21:00:10 -07:00
bolt: FlatHeadBolt = SHOULDER_AXIS_BOLT
2024-07-19 14:06:13 -07:00
height: float = 70.0
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18,
radius_rider=18,
groove_depth=4.8,
groove_radius_outer=16,
groove_radius_inner=13,
track_disk_height=5.0,
rider_disk_height=5.0,
radius_axle=3.0,
2024-07-19 21:00:10 -07:00
spring=SHOULDER_TORSION_SPRING,
2024-07-16 22:26:06 -07:00
rider_slot_begin=0,
rider_n_slots=1,
rider_slot_span=0,
))
# On the parent side, drill vertical holes
2024-07-23 16:49:25 -07:00
parent_conn_hole_diam: float = 4.0
# Position of the holes relative centre line
2024-07-17 00:30:41 -07:00
parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [
2024-07-23 16:49:25 -07:00
(20, 8),
(20, -8),
2024-07-17 00:30:41 -07:00
])
2024-07-23 16:49:25 -07:00
# Distance from centre of lips to the axis
parent_lip_ext: float = 40.0
2024-07-16 22:26:06 -07:00
parent_lip_length: float = 25.0
parent_lip_width: float = 30.0
parent_lip_thickness: float = 5.0
2024-07-23 16:49:25 -07:00
# The parent side has arms which connect to the lips
parent_arm_width: float = 25.0
parent_arm_height: float = 12.0
# Generates a child guard which covers up the internals. The lip length is
# relative to the +X surface of the guard.
child_guard_ext: float = 30.0
child_guard_width: float = 25.0
# guard length measured from axle
child_lip_length: float = 40.0
child_lip_width: float = 20.0
2024-07-23 16:49:25 -07:00
child_conn_hole_diam: float = 4.0
# Measured from centre of axle
2024-07-23 16:49:25 -07:00
child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30])
child_core_thickness: float = 3.0
2024-07-16 22:26:06 -07:00
# Rotates the torsion joint to avoid collisions or for some other purpose
2024-07-23 16:49:25 -07:00
axis_rotate_bot: float = 90
axis_rotate_top: float = 0
2024-07-16 22:26:06 -07:00
directrix_id: int = 0
2024-07-23 16:49:25 -07:00
angle_neutral: float = -15.0
2024-07-24 00:22:38 -07:00
angle_max_deflection: float = 65.0
2024-07-16 22:26:06 -07:00
2024-07-24 01:35:23 -07:00
spool_radius: float = 14.0
spool_groove_depth: float = 1.0
spool_base_height: float = 3.0
spool_height: float = 5.0
spool_groove_inset: float = 2.0
2024-07-16 22:26:06 -07:00
def __post_init__(self):
assert self.parent_lip_length * 2 < self.height
2024-07-24 01:35:23 -07:00
assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness
assert self.spool_base_height > self.spool_groove_depth
2024-07-16 22:26:06 -07:00
2024-07-19 14:06:13 -07:00
@property
def radius(self):
return self.torsion_joint.radius
2024-07-24 01:35:23 -07:00
@property
def draft_length(self):
"""
Amount of wires that need to draft on the spool
"""
return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection)
2024-07-23 16:49:25 -07:00
def parent_lip_loc(self, left: bool=True) -> Cq.Location:
2024-07-19 14:06:13 -07:00
"""
2d location of the arm surface on the parent side, relative to axle
"""
2024-07-23 16:49:25 -07:00
dy = self.parent_arm_width / 2
sign = 1 if left else -1
loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90)
return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir
@property
def _max_contraction_angle(self) -> float:
return self.angle_max_deflection + self.angle_neutral
def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid:
"""
Generates a cylindrical sector which cuts away overlapping regions of the child and parent
"""
aspect = self.child_guard_width / self.parent_arm_width
theta = math.radians(self._max_contraction_angle)
theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect))
angle = math.degrees(theta_p)
assert 0 <= angle <= 90
# outer radius of the cut, overestimated
cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2)
span = 180
result = (
Cq.Solid.makeCylinder(
height=self.height,
radius=cut_radius,
angleDegrees=span,
).cut(Cq.Solid.makeCylinder(
height=self.height,
radius=self.torsion_joint.radius,
))
)
if parent:
angle = - span - angle
else:
angle = self._max_contraction_angle - angle
result = result.located(Cq.Location((0,0,-self.height/2), (0,0,1), angle))
if mirror:
result = result.mirror('XZ')
return result
2024-07-19 14:06:13 -07:00
2024-07-16 22:26:06 -07:00
def parent(self, top: bool = False) -> Cq.Assembly:
joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root
2024-07-23 16:49:25 -07:00
assert self.parent_arm_width <= joint.radius_track * 2
assert self.parent_lip_ext > joint.radius_track
2024-07-23 16:49:25 -07:00
arm = (
Cq.Solid.makeBox(
2024-07-23 16:49:25 -07:00
self.parent_lip_ext + self.parent_lip_width / 2,
self.parent_arm_width,
self.parent_arm_height)
.located(Cq.Location((0, -self.parent_arm_width/2 , 0)))
.cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_arm_height))
.cut(self._contraction_cut_geometry(parent=True, mirror=top))
)
2024-07-23 16:49:25 -07:00
lip_args = dict(
2024-07-17 00:30:41 -07:00
length=self.parent_lip_length,
width=self.parent_lip_width,
2024-07-17 00:30:41 -07:00
thickness=self.parent_lip_thickness,
2024-07-23 16:49:25 -07:00
hole_diam=self.parent_conn_hole_diam,
generate_side_tags=False,
)
lip1 = MountingBox(
**lip_args,
holes=[
Hole(x=self.height / 2 - x, y=-y)
for x, y in self.parent_conn_hole_pos
],
)
lip2 = MountingBox(
**lip_args,
2024-07-17 00:30:41 -07:00
holes=[
Hole(x=self.height / 2 - x, y=y)
for x, y in self.parent_conn_hole_pos
],
)
2024-07-23 16:49:25 -07:00
lip_dy = self.parent_arm_width / 2 - self.parent_lip_thickness
# Flip so the lip's holes point to -X
loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90)
2024-07-23 16:49:25 -07:00
loc_dir1 = Cq.Location((0,lip_dy,0), (0, 0, 1), -90)
loc_dir2 = Cq.Location((0,-lip_dy,0), (0, 0, 1), 90)
loc_pos = Cq.Location((self.parent_lip_ext, 0, 0))
2024-07-16 22:26:06 -07:00
rot = -self.axis_rotate_top if top else self.axis_rotate_bot
2024-07-23 16:49:25 -07:00
lip_p_tag, lip_n_tag = "lip_right", "lip_left"
if not top:
lip_p_tag, lip_n_tag = lip_n_tag, lip_p_tag
result = (
Cq.Assembly()
2024-07-16 22:26:06 -07:00
.add(joint.track(), name="track",
loc=Cq.Location((0, 0, 0), (0, 0, 1), rot))
2024-07-23 16:49:25 -07:00
.add(arm, name="arm")
.add(lip1.generate(), name=lip_p_tag, loc=loc_pos * loc_dir1 * loc_axis)
.add(lip2.generate(), name=lip_n_tag, loc=loc_pos * loc_dir2 * loc_axis)
)
return result
2024-07-16 22:26:06 -07:00
@target(name="parent-bot")
def parent_bot(self) -> Cq.Assembly:
2024-07-24 01:35:23 -07:00
result = (
self.parent(top=False)
)
return result
2024-07-16 22:26:06 -07:00
@target(name="parent-top")
def parent_top(self) -> Cq.Assembly:
return self.parent(top=True)
@property
def child_height(self) -> float:
"""
Calculates the y distance between two joint surfaces on the child side
of the shoulder joint.
"""
joint = self.torsion_joint
return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height
2024-07-24 01:35:23 -07:00
def _spool(self) -> Cq.Workplane:
"""
Generates the spool piece which holds the line in tension
"""
t = self.spool_groove_depth
bulk = Cq.Solid.makeCylinder(
radius=self.spool_radius,
height=self.spool_height,
).located(Cq.Location((0, 0, self.spool_base_height)))
base = Cq.Solid.makeCylinder(
radius=self.spool_radius - t,
height=self.spool_base_height,
)
hole_x = self.spool_radius - (t + self.spool_groove_inset)
slot = Cq.Solid.makeBox(
length=t + self.spool_groove_inset,
width=t,
height=self.spool_base_height,
).located(Cq.Location((hole_x, -t/2, 0)))
hole = Cq.Solid.makeBox(
length=t,
width=t,
height=self.spool_height + self.spool_base_height,
).located(Cq.Location((hole_x, -t/2, 0)))
centre_hole = Cq.Solid.makeCylinder(
radius=self.torsion_joint.radius_axle,
height=self.spool_height + self.spool_base_height,
)
return bulk.fuse(base).cut(slot, hole, centre_hole)
2024-07-16 22:26:06 -07:00
@target(name="child")
def child(self) -> Cq.Assembly:
"""
Creates the top/bottom shoulder child joint
"""
joint = self.torsion_joint
assert all(r < self.child_lip_length for r in self.child_conn_hole_pos)
# Half of the height of the bridging cylinder
dh = self.height / 2 - joint.total_height
core_start_angle = 30
core_end_angle1 = 90
core_end_angle2 = 180
radius_core_inner = joint.radius_rider - self.child_core_thickness
core_profile1 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
.segment((0, 0))
.close()
.assemble()
.circle(radius_core_inner, mode='s')
)
core_profile2 = (
Cq.Sketch()
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle))
.segment((0, 0))
.close()
.assemble()
.circle(radius_core_inner, mode='s')
)
core_guard = (
Cq.Workplane('XY')
.box(
length=self.child_guard_ext,
width=self.child_guard_width,
height=self.height,
centered=(False, True, True),
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
radius=self.radius,
height=self.height,
combine='cut',
centered=True,
)
.copyWorkplane(Cq.Workplane('XY'))
.box(
length=self.child_guard_ext,
width=self.child_lip_width,
height=self.height - self.torsion_joint.total_height * 2,
combine='cut',
centered=(False, True, True),
)
2024-07-23 16:49:25 -07:00
.cut(self._contraction_cut_geometry(parent=False))
)
core = (
Cq.Workplane('XY')
.placeSketch(core_profile1)
.toPending()
.extrude(dh * 2)
.copyWorkplane(Cq.Workplane('XY'))
.placeSketch(core_profile2)
.toPending()
.extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh))
.union(core_guard)
)
assert self.child_lip_width / 2 <= joint.radius_rider
lip_thickness = joint.rider_disk_height
lip = box_with_centre_holes(
length=self.child_lip_length,
width=self.child_lip_width,
height=lip_thickness,
hole_loc=self.child_conn_hole_pos,
hole_diam=self.child_conn_hole_diam,
)
2024-07-16 22:26:06 -07:00
theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral)
loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral)
2024-07-24 01:35:23 -07:00
spool_dz = self.height / 2 - self.torsion_joint.total_height
spool_angle = 180 + self.angle_neutral
result = (
Cq.Assembly()
.add(core, name="core", loc=Cq.Location())
.add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top",
2024-07-16 22:26:06 -07:00
loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta))
.add(joint.rider(rider_slot_begin=180), name="rider_bot",
2024-07-16 22:26:06 -07:00
loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
.add(lip, name="lip_top",
loc=Cq.Location((self.child_guard_ext, 0, dh)))
.add(lip, name="lip_bot",
loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate)
2024-07-24 01:35:23 -07:00
.add(self._spool(), name="spool",
loc=Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle))
)
return result
@assembly()
2024-07-23 16:49:25 -07:00
def assembly(
self,
fastener_pos: float = 0.0,
deflection: float = 0.0,
2024-07-23 22:40:49 -07:00
ignore_fasteners: bool = False,
2024-07-23 16:49:25 -07:00
) -> Cq.Assembly:
assert deflection <= self.angle_max_deflection
2024-07-16 22:26:06 -07:00
directrix = self.directrix_id
mat = Material.RESIN_TRANSPERENT
mat_spring = Material.STEEL_SPRING
2024-07-19 15:06:57 -07:00
bolt_z = self.height / 2 + self.bolt.height_thread * (fastener_pos - 1)
result = (
Cq.Assembly()
.addS(self.child(), name="child",
role=Role.CHILD, material=mat)
.constrain("child/core", "Fixed")
2024-07-19 21:00:10 -07:00
.addS(self.torsion_joint.spring.assembly(deflection=-deflection), name="spring_top",
role=Role.DAMPING, material=mat_spring)
2024-07-16 22:26:06 -07:00
.addS(self.parent_top(),
name="parent_top",
role=Role.PARENT, material=mat)
2024-07-19 21:00:10 -07:00
.addS(self.torsion_joint.spring.assembly(deflection=deflection), name="spring_bot",
role=Role.DAMPING, material=mat_spring)
2024-07-16 22:26:06 -07:00
.addS(self.parent_bot(),
name="parent_bot",
role=Role.PARENT, material=mat)
)
2024-07-23 22:40:49 -07:00
if not ignore_fasteners:
(
result
# Fasteners
.addS(self.bolt.assembly(), name="bolt_top",
loc=Cq.Location((0, 0, bolt_z)))
.constrain("bolt_top?root", 'Fixed')
.addS(self.bolt.assembly(), name="bolt_bot",
loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180))
.constrain("bolt_bot?root", 'Fixed')
)
2024-07-16 22:26:06 -07:00
TorsionJoint.add_constraints(
result,
rider="child/rider_top",
track="parent_top/track",
spring="spring_top",
directrix=directrix)
TorsionJoint.add_constraints(
result,
rider="child/rider_bot",
track="parent_bot/track",
spring="spring_bot",
directrix=directrix)
return result.solve()
2024-07-11 22:29:05 -07:00
@dataclass
class Beam:
"""
A I-shaped spine with two feet
"""
foot_length: float = 40.0
foot_width: float = 20.0
foot_height: float = 5.0
spine_thickness: float = 4.0
spine_length: float = 10.0
total_height: float = 50.0
2024-07-17 01:22:05 -07:00
hole_diam: float = 6.0
2024-07-11 22:29:05 -07:00
# distance between the centres of the two holes
hole_dist: float = 24.0
def __post_init__(self):
assert self.spine_height > 0
assert self.hole_diam + self.hole_dist < self.foot_length
assert self.hole_dist - self.hole_diam >= self.spine_length
@property
def spine_height(self):
return self.total_height - self.foot_height * 2
def foot(self) -> Cq.Workplane:
"""
just one foot
"""
dx = self.hole_dist / 2
result = (
Cq.Workplane('XZ')
.box(self.foot_length, self.foot_width, self.foot_height,
centered=(True, True, False))
.faces(">Y")
.workplane()
.pushPoints([(dx, 0), (-dx, 0)])
.hole(self.hole_diam)
)
plane = result.faces(">Y").workplane()
plane.moveTo(dx, 0).tagPlane("conn1")
plane.moveTo(-dx, 0).tagPlane("conn0")
return result
2024-07-16 14:25:17 -07:00
def generate(self, flip: bool = False) -> Cq.Assembly:
2024-07-11 22:29:05 -07:00
beam = (
Cq.Workplane('XZ')
.box(self.spine_length, self.spine_thickness, self.spine_height)
)
h = self.spine_height / 2 + self.foot_height
2024-07-16 14:25:17 -07:00
tag_p, tag_n = "top", "bot"
if flip:
tag_p, tag_n = tag_n, tag_p
2024-07-11 22:29:05 -07:00
result = (
Cq.Assembly()
.add(beam, name="beam")
2024-07-16 14:25:17 -07:00
.add(self.foot(), name=tag_p,
2024-07-11 22:29:05 -07:00
loc=Cq.Location((0, h, 0)))
2024-07-16 14:25:17 -07:00
.add(self.foot(), name=tag_n,
loc=Cq.Location((0, -h, 0), (1, 0, 0), 180))
2024-07-11 22:29:05 -07:00
)
return result
2024-07-10 16:21:11 -07:00
@dataclass
class DiskJoint(Model):
"""
Sandwiched disk joint for the wrist and elbow
2024-07-18 14:03:01 -07:00
We embed a spring inside the joint, with one leg in the disk and one leg in
the housing. This provides torsion resistance.
2024-07-10 16:21:11 -07:00
"""
2024-07-19 21:00:10 -07:00
spring: TorsionSpring = ELBOW_TORSION_SPRING
2024-07-10 16:21:11 -07:00
radius_housing: float = 22.0
radius_disk: float = 20.0
radius_axle: float = 3.0
2024-07-17 14:47:22 -07:00
housing_thickness: float = 4.0
disk_thickness: float = 7.0
2024-07-18 14:03:01 -07:00
# Amount by which the wall carves in
2024-07-10 16:21:11 -07:00
wall_inset: float = 2.0
2024-07-18 14:03:01 -07:00
# Height of the spring hole; if you make it too short the spring can't enter
spring_tail_hole_height: float = 2.0
# Spring angle at 0 degrees of movement
spring_angle_at_0: float = 90.0
spring_slot_offset: float = 5.0
2024-07-10 16:21:11 -07:00
# Angular span of movement
movement_angle: float = 120.0
2024-07-21 22:16:18 -07:00
# leave some gap for cushion
movement_gap: float = 5.0
2024-07-10 16:21:11 -07:00
# Angular span of tongue on disk
tongue_span: float = 30.0
tongue_length: float = 10.0
generate_inner_wall: bool = False
def __post_init__(self):
super().__init__(name="disk-joint")
2024-07-18 14:03:01 -07:00
assert self.radius_housing > self.radius_disk > self.radius_axle
assert self.spring.height < self.housing_thickness + self.disk_thickness
2024-07-10 16:21:11 -07:00
assert self.housing_upper_carve_offset > 0
2024-07-18 14:03:01 -07:00
assert self.spring_tail_hole_height > self.spring.thickness
2024-07-10 16:21:11 -07:00
@property
2024-07-11 08:42:13 -07:00
def neutral_movement_angle(self) -> Optional[float]:
a = self.spring.angle_neutral - self.spring_angle_at_0
2024-07-11 08:42:13 -07:00
if 0 <= a and a <= self.movement_angle:
return a
return None
@property
def total_thickness(self) -> float:
2024-07-10 16:21:11 -07:00
return self.housing_thickness * 2 + self.disk_thickness
2024-07-18 14:03:01 -07:00
@property
def disk_bot_thickness(self) -> float:
"""
Pads the bottom of the disk up to spring height
"""
return max(0, self.disk_thickness + self.spring.thickness - self.spring.height)
2024-07-10 16:21:11 -07:00
@property
2024-07-11 08:42:13 -07:00
def opening_span(self) -> float:
2024-07-10 16:21:11 -07:00
return self.movement_angle + self.tongue_span
@property
2024-07-11 08:42:13 -07:00
def housing_upper_carve_offset(self) -> float:
2024-07-11 22:29:05 -07:00
"""
Distance between the spring track and the outside of the upper housing
"""
2024-07-18 14:03:01 -07:00
return self.spring_tail_hole_height + (self.disk_thickness - self.disk_bot_thickness) - self.spring.height
2024-07-10 16:21:11 -07:00
2024-07-11 22:29:05 -07:00
@property
def housing_upper_dz(self) -> float:
"""
Distance between the default upper housing location and the median line
"""
return self.total_thickness / 2 - self.housing_thickness
2024-07-17 14:47:22 -07:00
def _disk_cut(self) -> Cq.Workplane:
return (
2024-07-10 16:21:11 -07:00
Cq.Solid.makeBox(
length=self.spring.tail_length,
width=self.spring.thickness,
2024-07-18 14:03:01 -07:00
height=self.spring.height-self.disk_bot_thickness,
2024-07-10 16:21:11 -07:00
)
2024-07-18 14:03:01 -07:00
.located(Cq.Location((0, self.spring.radius_inner, self.disk_bot_thickness)))
.rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset)
)
2024-07-17 14:47:22 -07:00
@target(name="disk")
def disk(self) -> Cq.Workplane:
radius_tongue = self.radius_disk + self.tongue_length
tongue = (
Cq.Solid.makeCylinder(
height=self.disk_thickness,
radius=radius_tongue,
angleDegrees=self.tongue_span,
).cut(Cq.Solid.makeCylinder(
height=self.disk_thickness,
radius=self.radius_disk,
))
2024-07-10 16:21:11 -07:00
)
result = (
Cq.Workplane('XY')
.cylinder(
height=self.disk_thickness,
radius=self.radius_disk,
centered=(True, True, False)
)
.union(tongue, tol=TOL)
2024-07-10 16:21:11 -07:00
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.disk_thickness,
radius=self.spring.radius,
2024-07-10 16:21:11 -07:00
centered=(True, True, False),
combine='cut',
)
2024-07-17 14:47:22 -07:00
.cut(self._disk_cut())
2024-07-10 16:21:11 -07:00
)
plane = result.copyWorkplane(Cq.Workplane('XY'))
theta = math.radians(self.spring_slot_offset)
plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
2024-07-10 16:21:11 -07:00
plane.workplane(offset=self.disk_thickness).tagPlane("mate_top")
2024-07-18 14:03:01 -07:00
plane.workplane(offset=self.disk_bot_thickness).tagPlane("mate_spring")
2024-07-10 16:21:11 -07:00
result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot")
return result
def wall(self) -> Cq.Compound:
height = self.disk_thickness + self.wall_inset
wall = Cq.Solid.makeCylinder(
radius=self.radius_housing,
height=height,
2024-07-21 22:16:18 -07:00
angleDegrees=360 - self.opening_span - self.movement_gap*2,
2024-07-10 16:21:11 -07:00
).cut(Cq.Solid.makeCylinder(
radius=self.radius_disk,
height=height,
2024-07-21 22:16:18 -07:00
)).rotate((0, 0, 0), (0, 0, 1), self.opening_span+self.movement_gap)
2024-07-10 16:21:11 -07:00
return wall
@target(name="housing-lower")
def housing_lower(self) -> Cq.Workplane:
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.radius_housing,
height=self.housing_thickness,
centered=(True, True, False),
)
.cut(Cq.Solid.makeCylinder(
radius=self.radius_axle,
height=self.housing_thickness,
))
)
result.faces(">Z").tag("mate")
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
2024-07-10 16:21:11 -07:00
result = result.cut(
self
.wall()
2024-07-17 14:47:22 -07:00
.located(Cq.Location((0, 0, self.housing_thickness - self.wall_inset)))
2024-07-10 16:21:11 -07:00
#.rotate((0, 0, 0), (1, 0, 0), 180)
#.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness)))
)
return result
@target(name="housing-upper")
def housing_upper(self) -> Cq.Workplane:
carve_angle = -(self.spring_angle_at_0 - self.spring_slot_offset)
2024-07-10 16:21:11 -07:00
carve = (
Cq.Solid.makeCylinder(
radius=self.spring.radius,
2024-07-18 14:03:01 -07:00
height=self.spring_tail_hole_height,
2024-07-10 16:21:11 -07:00
).fuse(Cq.Solid.makeBox(
length=self.spring.tail_length,
width=self.spring.thickness,
2024-07-18 14:03:01 -07:00
height=self.spring_tail_hole_height,
).located(Cq.Location((0, -self.spring.radius, 0))))
).rotate((0, 0, 0), (0, 0, 1), carve_angle)
2024-07-10 16:21:11 -07:00
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.radius_housing,
height=self.housing_thickness,
centered=(True, True, False),
)
)
theta = math.radians(carve_angle)
2024-07-11 22:29:05 -07:00
result.faces("<Z").tag("mate")
p_xy = result.copyWorkplane(Cq.Workplane('XY'))
p_xy.tagPlane("dirX", direction="+X")
p_xy.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
2024-07-10 16:21:11 -07:00
result = result.faces(">Z").hole(self.radius_axle * 2)
# tube which holds the spring interior
if self.generate_inner_wall:
tube = (
Cq.Solid.makeCylinder(
radius=self.radius_spring_internal,
height=self.disk_thickness + self.housing_thickness,
).cut(Cq.Solid.makeCylinder(
radius=self.radius_axle,
height=self.disk_thickness + self.housing_thickness,
))
)
result = result.union(tube)
wall = (
self.wall()
2024-07-11 22:29:05 -07:00
.located(Cq.Location((0, 0, -self.disk_thickness-self.wall_inset)))
2024-07-10 16:21:11 -07:00
)
result = (
result
.union(wall, tol=TOL)
2024-07-18 14:03:01 -07:00
#.cut(carve)
2024-07-17 14:47:22 -07:00
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
2024-07-10 16:21:11 -07:00
)
return result.clean()
2024-07-10 16:21:11 -07:00
2024-07-11 08:42:13 -07:00
def add_constraints(self,
assembly: Cq.Assembly,
housing_lower: str,
housing_upper: str,
disk: str,
angle: float = 0.0,
2024-07-11 08:42:13 -07:00
) -> Cq.Assembly:
2024-07-16 14:25:17 -07:00
assert 0 <= angle <= self.movement_angle
deflection = angle - self.neutral_movement_angle
spring_name = disk.replace("/", "__Z") + "_spring"
(
2024-07-11 08:42:13 -07:00
assembly
.addS(
2024-07-19 21:00:10 -07:00
self.spring.assembly(deflection=-deflection),
name=spring_name,
role=Role.DAMPING,
material=Material.STEEL_SPRING)
2024-07-11 08:42:13 -07:00
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0)
.constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0)
.constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0)
2024-07-18 14:03:01 -07:00
.constrain(f"{disk}?mate_spring", f"{spring_name}?bot", "Plane")
#.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0)
#.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
#.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
)
return (
assembly
2024-07-11 08:42:13 -07:00
)
2024-07-10 16:21:11 -07:00
2024-07-11 08:42:13 -07:00
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
if angle is None:
angle = self.neutral_movement_angle
2024-07-11 08:42:13 -07:00
if angle is None:
angle = 0
else:
assert 0 <= angle <= self.movement_angle
2024-07-10 16:21:11 -07:00
result = (
Cq.Assembly()
.addS(self.disk(), name="disk", role=Role.CHILD)
.addS(self.housing_lower(), name="housing_lower", role=Role.PARENT)
.addS(self.housing_upper(), name="housing_upper", role=Role.CASING)
.constrain("housing_lower", "Fixed")
2024-07-10 16:21:11 -07:00
)
result = self.add_constraints(
2024-07-11 08:42:13 -07:00
result,
housing_lower="housing_lower",
housing_upper="housing_upper",
disk="disk",
angle=angle,
2024-07-11 08:42:13 -07:00
)
return result.solve()
2024-07-10 16:21:11 -07:00
2024-07-16 14:25:17 -07:00
@dataclass(kw_only=True)
class ElbowJoint(Model):
2024-07-11 22:29:05 -07:00
"""
Creates the elbow and wrist joints.
This consists of a disk joint, where each side of the joint has mounting
holes for connection to the exoskeleton. Each side 2 mounting feet on the
top and bottom, and each foot has 2 holes.
On the parent side, additional bolts are needed to clamp the two sides of
the housing together.
"""
disk_joint: DiskJoint = field(default_factory=lambda: DiskJoint(
movement_angle=60,
))
# Distance between the child/parent arm to the centre
child_arm_radius: float = 40.0
parent_arm_radius: float = 40.0
2024-07-17 10:22:59 -07:00
lip_thickness: float = 5.0
lip_length: float = 60.0
hole_pos: list[float] = field(default_factory=lambda: [15, 25])
2024-07-17 14:47:22 -07:00
parent_arm_width: float = 10.0
2024-07-11 22:29:05 -07:00
# Angle of the beginning of the parent arm
parent_arm_angle: float = 180.0
# Size of the mounting holes
2024-07-19 14:06:13 -07:00
hole_diam: float = 4.0
2024-07-11 22:29:05 -07:00
material: Material = Material.RESIN_TRANSPERENT
2024-07-23 19:13:06 -07:00
# If set to true, the joint is flipped upside down.
flip: bool = False
2024-07-18 14:03:01 -07:00
angle_neutral: float = 30.0
2024-07-16 14:25:17 -07:00
2024-07-22 09:49:16 -07:00
actuator: Optional[LinearActuator]
flexor: Optional[Flexor] = None
# Rotates the entire flexor
flexor_offset_angle: float = 0
# Rotates the surface of the mount
2024-07-23 19:13:06 -07:00
flexor_mount_rot: float = 0
2024-07-11 22:29:05 -07:00
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
2024-07-22 09:49:16 -07:00
if self.actuator:
self.flexor = Flexor(
actuator=self.actuator,
motion_span=self.motion_span
)
2024-07-11 22:29:05 -07:00
2024-07-19 14:06:13 -07:00
@property
def total_thickness(self):
return self.disk_joint.total_thickness
@property
def motion_span(self) -> float:
return self.disk_joint.movement_angle
2024-07-18 14:03:01 -07:00
def parent_arm_loc(self) -> Cq.Location:
"""
2d Location of the centre of the arm surface on the parent side, assuming
axle is at position 0, and parent direction is -X
"""
return Cq.Location.from2d(-self.parent_arm_radius, 0, 0)
2024-07-23 19:13:06 -07:00
def child_arm_loc(self, angle: float = 0.0) -> Cq.Location:
2024-07-18 14:03:01 -07:00
"""
2d Location of the centre of the arm surface on the child side, assuming
axle is at position 0, and parent direction is -X
"""
result = Cq.Location.rot2d(self.angle_neutral + angle) * Cq.Location.from2d(self.child_arm_radius, 0, 180)
2024-07-23 19:13:06 -07:00
return result.flip_y() if self.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()
2024-07-23 19:13:06 -07:00
def actuator_mount_loc(
self,
child: bool = False,
# If set to true, use the local coordinate system
unflip: bool = False,
) -> Cq.Location:
# 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
mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos()
loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0)
2024-07-24 00:22:38 -07:00
alpha = (-mount_loc_angle if child else 0) + 180 - self.flexor_offset_angle
loc_rot = Cq.Location.rot2d(alpha)
2024-07-23 19:13:06 -07:00
loc = loc_rot * loc_span * loc_mount_orient * loc_mount
return loc.flip_y() if self.flip and not child and not unflip else loc
2024-07-18 14:03:01 -07:00
2024-07-17 10:22:59 -07:00
def lip(self) -> Cq.Workplane:
2024-07-23 19:13:06 -07:00
sign = -1 if self.flip else 1
2024-07-17 10:22:59 -07:00
holes = [
h
for i, x in enumerate(self.hole_pos)
for h in [
2024-07-23 19:13:06 -07:00
Hole(x=sign * x, tag=f"conn_top{i}"),
Hole(x=-sign * x, tag=f"conn_bot{i}")
2024-07-17 10:22:59 -07:00
]
]
mbox = MountingBox(
length=self.lip_length,
width=self.disk_joint.total_thickness,
thickness=self.lip_thickness,
holes=holes,
hole_diam=self.hole_diam,
centred=(True, True),
generate_side_tags=False,
generate_reverse_tags=True,
2024-07-17 10:22:59 -07:00
)
return mbox.generate()
@target(name="child")
2024-07-11 22:29:05 -07:00
def child_joint(self) -> Cq.Assembly:
angle = -self.disk_joint.tongue_span / 2
dz = self.disk_joint.disk_thickness / 2
# We need to ensure the disk is on the "other" side so
flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180)
flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180)
lip_dz = self.lip_thickness
2024-07-17 14:47:22 -07:00
loc_lip = (
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)
)
2024-07-24 00:22:38 -07:00
loc_rot_neutral = Cq.Location.rot2d(self.angle_neutral)
2024-07-23 19:13:06 -07:00
loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0))
2024-07-18 14:03:01 -07:00
loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness))
2024-07-17 14:47:22 -07:00
disk_cut = self.disk_joint._disk_cut().located(
2024-07-18 14:03:01 -07:00
loc_lip.inverse * loc_cut_rel * loc_disk)
2024-07-11 22:29:05 -07:00
result = (
2024-07-17 10:22:59 -07:00
Cq.Assembly()
2024-07-24 00:22:38 -07:00
.add(self.disk_joint.disk(), name="disk",
loc=loc_rot_neutral * Cq.Location((0, 0, -dz), (0,0,1), angle))
.add(self.lip().cut(disk_cut), name="lip",
loc=loc_rot_neutral * loc_disk.inverse * loc_lip)
2024-07-11 22:29:05 -07:00
)
2024-07-23 19:13:06 -07:00
# Orientes the hole surface so it faces +X
loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90)
2024-07-22 09:49:16 -07:00
if self.flexor:
2024-07-23 19:13:06 -07:00
result.add(
self.actuator_mount(),
name="act",
loc=self.actuator_mount_loc(child=True) * loc_thickness)
2024-07-11 22:29:05 -07:00
return result
@target(name="parent-lower")
def parent_joint_lower(self) -> Cq.Workplane:
2024-07-11 22:29:05 -07:00
return self.disk_joint.housing_lower()
@target(name="parent-upper")
2024-07-23 22:40:49 -07:00
def parent_joint_upper(self, generate_mount: bool=False):
2024-07-11 22:29:05 -07:00
axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
housing_dz = self.disk_joint.housing_upper_dz
2024-07-17 14:47:22 -07:00
conn_h = self.disk_joint.total_thickness
conn_w = self.parent_arm_width
2024-07-11 22:29:05 -07:00
connector = (
2024-07-17 14:47:22 -07:00
Cq.Solid.makeBox(
length=self.parent_arm_radius,
width=conn_w,
2024-07-11 22:29:05 -07:00
height=conn_h,
2024-07-17 14:47:22 -07:00
).located(Cq.Location((0, -conn_w/2, 0)))
#Cq.Solid.makeCylinder(
# height=conn_h,
# radius=self.parent_arm_radius - self.lip_thickness / 2,
# angleDegrees=self.parent_arm_span)
2024-07-11 22:29:05 -07:00
.cut(Cq.Solid.makeCylinder(
height=conn_h,
radius=self.disk_joint.radius_housing,
))
.located(Cq.Location((0, 0, -conn_h / 2)))
2024-07-17 14:47:22 -07:00
.rotate((0,0,0), (0,0,1), 180)
#.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2)
2024-07-11 22:29:05 -07:00
)
housing = self.disk_joint.housing_upper()
housing_loc = Cq.Location(
2024-07-23 19:13:06 -07:00
(0, 0, housing_dz),
(0, 0, 1),
2024-07-17 10:22:59 -07:00
-self.disk_joint.tongue_span / 2 + self.angle_neutral
)
lip_dz = self.lip_thickness
2024-07-11 22:29:05 -07:00
result = (
2024-07-17 10:22:59 -07:00
Cq.Assembly()
2024-07-23 19:13:06 -07:00
# rotate so 0 degree is at +X
.add(housing, name="housing", loc=housing_loc)
2024-07-17 10:22:59 -07:00
.add(self.lip(), name="lip", loc=
2024-07-23 19:13:06 -07:00
axial_offset.inverse *
2024-07-17 10:22:59 -07:00
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))
2024-07-23 19:13:06 -07:00
.add(connector, name="connector")
#.constrain("housing", "Fixed")
#.constrain("connector", "Fixed")
#.solve()
2024-07-11 22:29:05 -07:00
)
2024-07-22 09:49:16 -07:00
if self.flexor:
2024-07-23 22:40:49 -07:00
if generate_mount:
# Orientes the hole surface so it faces +X
loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90)
result.add(
self.actuator_mount(),
name="act",
2024-07-24 00:22:38 -07:00
loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness)
2024-07-23 22:40:49 -07:00
else:
result.add(
Cq.Edge.makeLine((-1,0,0), (1,0,0)),
name="act",
2024-07-24 00:22:38 -07:00
loc=self.actuator_mount_loc(child=False, unflip=True))
2024-07-11 22:29:05 -07:00
return result
@assembly()
2024-07-23 22:40:49 -07:00
def assembly(self,
angle: float = 0,
generate_mount: bool = False,
ignore_actuators: bool = False) -> Cq.Assembly:
assert 0 <= angle <= self.motion_span
2024-07-11 22:29:05 -07:00
result = (
Cq.Assembly()
.addS(self.child_joint(), name="child",
role=Role.CHILD, material=self.material)
.addS(self.parent_joint_lower(), name="parent_lower",
role=Role.CASING, material=self.material)
2024-07-23 22:40:49 -07:00
.addS(self.parent_joint_upper(generate_mount=generate_mount), name="parent_upper",
role=Role.PARENT, material=self.material)
#.constrain("child/disk?mate_bot", "Fixed")
2024-07-11 22:29:05 -07:00
)
result = self.disk_joint.add_constraints(
2024-07-11 22:29:05 -07:00
result,
housing_lower="parent_lower",
housing_upper="parent_upper/housing",
2024-07-11 22:29:05 -07:00
disk="child/disk",
angle=angle,
2024-07-11 22:29:05 -07:00
)
2024-07-23 22:40:49 -07:00
if not ignore_actuators and self.flexor:
target_length = self.flexor.target_length_at_angle(
angle=angle,
)
self.flexor.add_to(
result,
target_length=target_length,
2024-07-23 22:40:49 -07:00
tag_hole_back="parent_upper/act",
tag_hole_front="child/act?mount",
tag_dir="parent_lower?mate",
)
2024-07-11 22:29:05 -07:00
return result.solve()
2024-07-10 16:21:11 -07:00
if __name__ == '__main__':
p = ShoulderJoint()
p.build_all()
2024-07-10 16:21:11 -07:00
p = DiskJoint()
p.build_all()