Cosplay/nhf/touhou/houjuu_nue/parts.py

262 lines
8.7 KiB
Python

from dataclasses import dataclass
from typing import Optional, Tuple
import cadquery as Cq
from nhf import Role
from nhf.build import Model, target
import nhf.parts.springs as springs
import nhf.utils
TOL = 1e-6
@dataclass
class DiskJoint(Model):
"""
Sandwiched disk joint for the wrist and elbow
"""
radius_housing: float = 22.0
radius_disk: float = 20.0
radius_spring: float = 9 / 2
radius_axle: float = 3.0
housing_thickness: float = 5.0
disk_thickness: float = 5.0
# Gap between disk and the housing
#disk_thickness_gap: float = 0.1
spring_thickness: float = 1.3
spring_height: float = 6.5
spring_tail_length: float = 45.0
# Spring angle at 0 degrees of movement
spring_angle: float = 30.0
# Angle at which the spring exerts no torque
spring_angle_neutral: float = 90.0
spring_angle_shift: float = 30
wall_inset: float = 2.0
# Angular span of movement
movement_angle: float = 120.0
# 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")
assert self.housing_thickness > self.wall_inset
assert self.radius_housing > self.radius_disk
assert self.radius_disk > self.radius_axle
assert self.housing_upper_carve_offset > 0
def spring(self):
return springs.torsion_spring(
radius=self.radius_spring,
height=self.spring_height,
thickness=self.spring_thickness,
tail_length=self.spring_tail_length,
right_handed=False,
)
@property
def neutral_movement_angle(self) -> Optional[float]:
a = self.spring_angle_neutral - self.spring_angle
if 0 <= a and a <= self.movement_angle:
return a
return None
@property
def total_thickness(self) -> float:
return self.housing_thickness * 2 + self.disk_thickness
@property
def opening_span(self) -> float:
return self.movement_angle + self.tongue_span
@property
def housing_upper_carve_offset(self) -> float:
return self.housing_thickness + self.disk_thickness - self.spring_height
@property
def radius_spring_internal(self):
return self.radius_spring - self.spring_thickness
@target(name="disk")
def disk(self) -> Cq.Workplane:
cut = (
Cq.Solid.makeBox(
length=self.spring_tail_length,
width=self.spring_thickness,
height=self.disk_thickness,
)
.located(Cq.Location((0, self.radius_spring_internal, 0)))
.rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift)
)
result = (
Cq.Workplane('XY')
.cylinder(
height=self.disk_thickness,
radius=self.radius_disk,
centered=(True, True, False)
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.disk_thickness,
radius=self.radius_spring,
centered=(True, True, False),
combine='cut',
)
.cut(cut)
)
plane = result.copyWorkplane(Cq.Workplane('XY'))
plane.tagPlane("dir", direction="+X")
plane.workplane(offset=self.disk_thickness).tagPlane("mate_top")
result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot")
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,
))
)
result = result.union(tongue, tol=TOL)
return result
def wall(self) -> Cq.Compound:
height = self.disk_thickness + self.wall_inset
wall = Cq.Solid.makeCylinder(
radius=self.radius_housing,
height=height,
angleDegrees=360 - self.opening_span,
).cut(Cq.Solid.makeCylinder(
radius=self.radius_disk,
height=height,
)).rotate((0, 0, 0), (0, 0, 1), self.opening_span)
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("dir", direction="+X")
result = result.cut(
self
.wall()
.located(Cq.Location((0, 0, self.disk_thickness - self.wall_inset)))
#.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 = (
Cq.Solid.makeCylinder(
radius=self.radius_spring,
height=self.housing_thickness
).fuse(Cq.Solid.makeBox(
length=self.spring_tail_length,
width=self.spring_thickness,
height=self.housing_thickness
).located(Cq.Location((0, self.radius_spring_internal, 0))))
).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift)
result = (
Cq.Workplane('XY')
.cylinder(
radius=self.radius_housing,
height=self.housing_thickness,
centered=(True, True, False),
)
)
result.faces(">Z").tag("mate")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
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()
.rotate((0, 0, 0), (0, 0, 1), self.tongue_span)
.mirror("XY")
.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset)))
)
result = (
result
.union(wall, tol=TOL)
.cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset))))
)
return result
def add_constraints(self,
assembly: Cq.Assembly,
housing_lower: str,
housing_upper: str,
disk: str,
angle: Tuple[float, float, float] = (0, 0, 0),
) -> Cq.Assembly:
"""
The angle supplied must be perpendicular to the disk normal.
"""
(
assembly
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
.constrain(f"{housing_lower}?dir", f"{housing_upper}?dir", "Axis")
.constrain(f"{disk}?dir", "FixedRotation", angle)
)
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
if angle is None:
angle = self.movement_angle
if angle is None:
angle = 0
else:
assert 0 <= angle <= self.movement_angle
result = (
Cq.Assembly()
.add(self.disk(), name="disk", color=Role.CHILD.color)
.add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color)
.add(self.housing_upper(), name="housing_upper", color=Role.CASING.color)
.constrain("housing_lower", "Fixed")
)
self.add_constraints(
result,
housing_lower="housing_lower",
housing_upper="housing_upper",
disk="disk",
angle=(0, 0, angle),
)
return result.solve()
if __name__ == '__main__':
p = DiskJoint()
p.build_all()