fix: Spool obstruction problem, elbow fasteners

This commit is contained in:
Leni Aniva 2024-08-03 23:31:36 -07:00
parent c2161b6171
commit 5eeeace852
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
4 changed files with 186 additions and 51 deletions

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass
import math
import cadquery as Cq
from typing import Optional
from nhf import Item, Role
import nhf.utils
@ -127,3 +128,35 @@ class HexNut(Item):
.regularPolygon(r=self.radius, n=6)
._faces
)
@dataclass(frozen=True)
class Washer(Item):
diam_thread: float
diam_outer: float
thickness: float
material_name: Optional[float] = None
def __post_init__(self):
assert self.diam_outer > self.diam_thread
@property
def name(self):
suffix = (" " + self.material_name) if self.material_name else ""
return f"Washer M{int(self.diam_thread)}{suffix}"
@property
def role(self) -> Role:
return Role.CONNECTION
def generate(self) -> Cq.Workplane:
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.diam_outer/2,
height=self.thickness,
)
.faces(">Z")
.hole(self.diam_thread)
)
result.faces("<Z").tag("bot")
result.faces(">Z").tag("top")
return result

View File

@ -547,3 +547,9 @@ class ElectronicBoard(Model):
)
)
return result.solve()
@dataclass(frozen=True)
class LightStrip:
width: float = 10.0
height: float = 4.5

View File

@ -6,7 +6,7 @@ from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.box import MountingBox
from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer
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 (
@ -67,6 +67,28 @@ ELBOW_TORSION_SPRING = TorsionSpring(
right_handed=False,
)
ELBOW_AXLE_BOLT = FlatHeadBolt(
mass=0.0,
diam_head=6.87,
height_head=3.06,
diam_thread=4.0,
height_thread=20.0,
)
ELBOW_AXLE_WASHER = Washer(
mass=0.0,
diam_outer=8.96,
diam_thread=4.0,
thickness=1.02,
material_name="Nylon"
)
ELBOW_AXLE_HEX_NUT = HexNut(
mass=0.0,
diam_thread=4.0,
pitch=0.7,
thickness=3.6, # or 2.64 for metal
width=6.89,
)
@dataclass
class RootJoint(Model):
"""
@ -390,12 +412,13 @@ class ShoulderJoint(Model):
angle_neutral: float = -15.0
angle_max_deflection: float = 65.0
spool_radius: float = 12.0
spool_groove_depth: float = 1.0
spool_base_height: float = 3.0
spool_height: float = 5.0
spool_cap_height: float = 1.0
spool_groove_inset: float = 3.0
spool_radius_diff: float = 2.0
# All the heights here are mirrored for the bottom as well
spool_cap_height: float = 3.0
spool_core_height: float = 2.0
spool_line_thickness: float = 1.2
spool_groove_radius: float = 10.0
flip: bool = False
actuator: LinearActuator = LINEAR_ACTUATOR_21
@ -403,11 +426,17 @@ class ShoulderJoint(Model):
def __post_init__(self):
assert self.parent_lip_length * 2 < self.height
assert self.child_guard_ext > self.torsion_joint.radius_rider
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
assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius
assert self.child_lip_height < self.height
assert self.draft_length <= self.actuator.stroke_length
@property
def spool_outer_radius(self):
return self.torsion_joint.radius_rider - self.child_core_thickness
@property
def spool_inner_radius(self):
return self.spool_outer_radius - self.spool_radius_diff
@property
def radius(self):
return self.torsion_joint.radius
@ -417,7 +446,7 @@ class ShoulderJoint(Model):
"""
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)
return self.spool_inner_radius * math.radians(self.angle_max_deflection)
@property
def draft_height(self):
@ -444,16 +473,20 @@ class ShoulderJoint(Model):
def _max_contraction_angle(self) -> float:
return 180 - 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
"""
def _contraction_cut_angle(self) -> float:
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))
theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect)
angle = math.degrees(theta_p)
assert 0 <= angle <= 90
return angle
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
"""
angle = self._contraction_cut_angle()
# outer radius of the cut, overestimated
cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2)
span = 180
@ -550,37 +583,39 @@ class ShoulderJoint(Model):
joint = self.torsion_joint
return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height
def _spool(self) -> Cq.Workplane:
def _spool(self) -> Cq.Compound:
"""
Generates the spool piece which holds the line in tension
"""
t = self.spool_groove_depth
radius_core_inner = self.torsion_joint.radius_rider - self.child_core_thickness
t = self.spool_line_thickness
spindle = Cq.Solid.makeCone(
radius1=self.spool_radius,
radius2=radius_core_inner,
height=self.spool_height,
radius1=self.spool_inner_radius,
radius2=self.spool_outer_radius,
height=self.spool_core_height,
)
cap = Cq.Solid.makeCylinder(
radius=radius_core_inner,
radius=self.spool_outer_radius,
height=self.spool_cap_height
).located(Cq.Location((0,0,self.spool_height)))
hole_x = radius_core_inner - self.spool_groove_inset
slot = Cq.Solid.makeBox(
length=t,
).moved(Cq.Location((0,0,self.spool_core_height)))
cut_height = self.spool_cap_height + self.spool_core_height
cut_hole = Cq.Solid.makeCylinder(
radius=t / 2,
height=cut_height,
).moved(Cq.Location((self.spool_groove_radius, 0, 0)))
cut_slot = Cq.Solid.makeBox(
length=self.spool_outer_radius - self.spool_groove_radius,
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(
height=self.spool_core_height,
).moved(Cq.Location((self.spool_groove_radius, -t/2, 0)))
cut_centre_hole = Cq.Solid.makeCylinder(
radius=self.torsion_joint.radius_axle,
height=self.spool_height + self.spool_base_height,
height=cut_height,
)
top = spindle.fuse(cap).cut(cut_hole, cut_centre_hole, cut_slot)
return (
top
.fuse(top.located(Cq.Location((0,0,0), (1,0, 0), 180)))
)
return spindle.fuse(cap).cut(slot, hole, centre_hole)
@target(name="child")
def child(self) -> Cq.Assembly:
@ -611,6 +646,14 @@ class ShoulderJoint(Model):
.assemble()
.circle(radius_core_inner, mode='s')
)
angle_line_span = -self.angle_neutral + self.angle_max_deflection + 90
angle_line = 180 - angle_line_span
# leave space for the line to rotate
spool_cut = Cq.Solid.makeCylinder(
radius=joint.radius_rider * 2,
height=self.spool_core_height * 2,
angleDegrees=angle_line_span,
).moved(Cq.Location((0,0,-self.spool_core_height), (0,0,1), angle_line))
lip_extension = (
Cq.Solid.makeBox(
length=self.child_lip_ext - self.child_guard_ext,
@ -676,7 +719,8 @@ class ShoulderJoint(Model):
.toPending()
.extrude(dh * 2)
.translate(Cq.Vector(0, 0, -dh))
.union(core_guard)
.cut(spool_cut)
.union(core_guard, tol=TOL)
)
assert self.child_lip_width / 2 <= joint.radius_rider
sign = 1 if self.flip else -1
@ -695,8 +739,8 @@ class ShoulderJoint(Model):
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)
spool_dz = self.height / 2 - self.torsion_joint.total_height
spool_angle = 180 + self.angle_neutral
spool_dz = 0
spool_angle = -self.angle_neutral
loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location()
result = (
Cq.Assembly()
@ -848,6 +892,7 @@ class DiskJoint(Model):
housing_thickness: float = 4.0
disk_thickness: float = 8.0
tongue_thickness: float = 10.0
# Amount by which the wall carves in
wall_inset: float = 2.0
@ -868,6 +913,9 @@ class DiskJoint(Model):
generate_inner_wall: bool = False
axle_bolt: FlatHeadBolt = ELBOW_AXLE_BOLT
axle_washer: Washer = ELBOW_AXLE_WASHER
axle_hex_nut: HexNut = ELBOW_AXLE_HEX_NUT
def __post_init__(self):
super().__init__(name="disk-joint")
@ -876,6 +924,11 @@ class DiskJoint(Model):
assert self.housing_upper_carve_offset > 0
assert self.spring_tail_hole_height > self.spring.thickness
assert self.tongue_thickness <= self.total_thickness
assert self.axle_bolt.diam_thread == self.axle_washer.diam_thread
assert self.axle_bolt.diam_thread == self.axle_hex_nut.diam_thread
assert self.axle_bolt.height_thread > self.total_thickness, "Bolt is not long enough"
@property
def neutral_movement_angle(self) -> Optional[float]:
@ -932,10 +985,21 @@ class DiskJoint(Model):
@target(name="disk")
def disk(self) -> Cq.Workplane:
radius_tongue = self.radius_disk + self.tongue_length
tongue = (
outer_tongue = (
Cq.Solid.makeCylinder(
height=self.tongue_thickness,
radius=radius_tongue,
angleDegrees=self.tongue_span,
).cut(Cq.Solid.makeCylinder(
height=self.tongue_thickness,
radius=self.radius_housing,
))
.moved(Cq.Location((0,0,(self.disk_thickness - self.tongue_thickness) / 2)))
)
inner_tongue = (
Cq.Solid.makeCylinder(
height=self.disk_thickness,
radius=radius_tongue,
radius=self.radius_housing,
angleDegrees=self.tongue_span,
).cut(Cq.Solid.makeCylinder(
height=self.disk_thickness,
@ -949,7 +1013,8 @@ class DiskJoint(Model):
radius=self.radius_disk,
centered=(True, True, False)
)
.union(tongue, tol=TOL)
.union(inner_tongue, tol=TOL)
.union(outer_tongue, tol=TOL)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.disk_thickness,
@ -995,6 +1060,7 @@ class DiskJoint(Model):
))
)
result.faces(">Z").tag("mate")
result.faces("<Z").tag("bot")
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
result = result.cut(
self
@ -1028,6 +1094,7 @@ class DiskJoint(Model):
)
theta = math.radians(carve_angle)
result.faces("<Z").tag("mate")
result.faces(">Z").tag("top")
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))
@ -1063,6 +1130,8 @@ class DiskJoint(Model):
housing_upper: str,
disk: str,
angle: float = 0.0,
fasteners: bool = True,
fastener_prefix: str = "fastener",
) -> Cq.Assembly:
assert 0 <= angle <= self.movement_angle
deflection = angle - (self.spring.angle_neutral - self.spring_angle_at_0)
@ -1084,6 +1153,16 @@ class DiskJoint(Model):
#.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
#.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
)
if fasteners:
tag_bolt = f"{fastener_prefix}_bolt"
tag_nut = f"{fastener_prefix}_nut"
(
assembly
.add(self.axle_bolt.assembly(), name=tag_bolt)
.add(self.axle_hex_nut.assembly(), name=tag_nut)
.constrain(f"{housing_lower}?bot", f"{tag_nut}?bot", "Plane")
.constrain(f"{housing_upper}?top", f"{tag_bolt}?root", "Plane", param=0)
)
return (
assembly
)
@ -1136,7 +1215,9 @@ class ElbowJoint(Model):
lip_thickness: float = 5.0
lip_length: float = 60.0
hole_pos: list[float] = field(default_factory=lambda: [12, 24])
# Carve which allows light to go through
lip_side_depression_width: float = 10.0
hole_pos: list[float] = field(default_factory=lambda: [15, 24])
parent_arm_width: float = 10.0
# Angle of the beginning of the parent arm
parent_arm_angle: float = 180.0
@ -1254,6 +1335,17 @@ class ElbowJoint(Model):
Hole(x=-sign * x, tag=f"conn_bot{i}")
]
]
def post(sketch: Cq.Sketch) -> Cq.Sketch:
y_outer = self.disk_joint.total_thickness / 2
y_inner = self.disk_joint.tongue_thickness / 2
y = (y_outer + y_inner) / 2
width = self.lip_side_depression_width
height = y_outer - y_inner
return (
sketch
.push([(0, y), (0, -y)])
.rect(width, height, mode='s')
)
mbox = MountingBox(
length=self.lip_length,
width=self.disk_joint.total_thickness,
@ -1263,6 +1355,7 @@ class ElbowJoint(Model):
centred=(True, True),
generate_side_tags=False,
generate_reverse_tags=True,
profile_callback=post,
)
return mbox.generate()
@ -1332,7 +1425,7 @@ class ElbowJoint(Model):
def parent_joint_upper(self, generate_mount: bool=False, generate_tags=True):
axial_offset = Cq.Location((self.parent_arm_radius, 0, 0))
housing_dz = self.disk_joint.housing_upper_dz
conn_h = self.disk_joint.total_thickness
conn_h = self.disk_joint.tongue_thickness
conn_w = self.parent_arm_width
connector = (
Cq.Solid.makeBox(

View File

@ -19,6 +19,7 @@ from nhf.touhou.houjuu_nue.electronics import (
LINEAR_ACTUATOR_21,
LINEAR_ACTUATOR_50,
ElectronicBoard,
LightStrip,
ELECTRONIC_MOUNT_HEXNUT,
)
import nhf.utils
@ -52,6 +53,8 @@ class WingProfile(Model):
spacer_thickness: float = 25.4 / 4
rod_width: float = 10.0
light_strip: LightStrip = LightStrip()
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
))
shoulder_angle_bias: float = 0.0
@ -370,9 +373,8 @@ class WingProfile(Model):
)
@submodel(name="spacer-s0-shoulder-act")
def spacer_s0_shoulder_act(self) -> MountingBox:
dx = self.shoulder_joint.draft_height
return MountingBox(
holes=[Hole(x=dx), Hole(x=-dx)],
holes=[Hole(x=0)],
hole_diam=self.shoulder_joint.actuator.back_hole_diam,
length=self.root_height,
width=10.0,
@ -616,16 +618,17 @@ class WingProfile(Model):
Hole(sign * x, tag=tag)
for x, tag in joint.hole_loc_tags()
]
tongue_thickness = joint.disk_joint.tongue_thickness
carve_width = joint.lip_side_depression_width
assert carve_width >= self.light_strip.width
carve_height = (segment_thickness - tongue_thickness) / 2
assert carve_height >= self.light_strip.height
def carve_sides(profile):
dy = (segment_thickness + joint.total_thickness) / 4
dy = (segment_thickness + tongue_thickness) / 4
return (
profile
.push([(0,-dy), (0,dy)])
.rect(
joint.parent_arm_width,
(segment_thickness - joint.total_thickness) / 2,
mode='s',
)
.rect(carve_width, carve_height, mode='s')
)
# FIXME: Carve out the sides so light can pass through
mbox = MountingBox(