fix: Spool obstruction problem, elbow fasteners
This commit is contained in:
parent
c2161b6171
commit
5eeeace852
|
@ -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
|
||||
|
|
|
@ -547,3 +547,9 @@ class ElectronicBoard(Model):
|
|||
)
|
||||
)
|
||||
return result.solve()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LightStrip:
|
||||
|
||||
width: float = 10.0
|
||||
height: float = 4.5
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue