2024-06-26 12:57:22 -07:00
|
|
|
import math
|
2024-07-16 13:28:53 -07:00
|
|
|
from typing import Optional
|
|
|
|
from dataclasses import dataclass
|
2024-06-26 12:57:22 -07:00
|
|
|
import cadquery as Cq
|
2024-07-19 14:06:13 -07:00
|
|
|
from nhf import Item, Role
|
2024-06-26 12:57:22 -07:00
|
|
|
|
2024-07-19 14:06:13 -07:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
class TorsionSpring(Item):
|
2024-06-26 12:57:22 -07:00
|
|
|
"""
|
2024-07-16 13:28:53 -07:00
|
|
|
A torsion spring with abridged geometry (since sweep is slow)
|
2024-06-26 12:57:22 -07:00
|
|
|
"""
|
2024-07-16 13:28:53 -07:00
|
|
|
# Outer radius
|
|
|
|
radius: float = 12.0
|
|
|
|
height: float = 20.0
|
|
|
|
thickness: float = 2.0
|
|
|
|
|
|
|
|
# Angle (in degrees) between the two legs at neutral position
|
|
|
|
angle_neutral: float = 90.0
|
|
|
|
|
|
|
|
tail_length: float = 25.0
|
|
|
|
right_handed: bool = False
|
|
|
|
|
|
|
|
torsion_rate: Optional[float] = None
|
|
|
|
|
2024-07-19 14:06:13 -07:00
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return f"TorsionSpring-{int(self.radius)}-{int(self.height)}"
|
|
|
|
|
2024-07-16 13:28:53 -07:00
|
|
|
@property
|
|
|
|
def radius_inner(self) -> float:
|
|
|
|
return self.radius - self.thickness
|
|
|
|
|
|
|
|
def torque_at(self, theta: float) -> float:
|
|
|
|
return self.torsion_rate * theta
|
|
|
|
|
2024-07-19 14:06:13 -07:00
|
|
|
def generate(self, deflection: float = 0) -> Cq.Workplane:
|
2024-07-16 13:28:53 -07:00
|
|
|
omega = self.angle_neutral + deflection
|
|
|
|
omega = -omega if self.right_handed else omega
|
|
|
|
base = (
|
|
|
|
Cq.Workplane('XY')
|
|
|
|
.cylinder(height=self.height, radius=self.radius,
|
|
|
|
centered=(True, True, False))
|
|
|
|
)
|
|
|
|
base.faces(">Z").tag("top")
|
|
|
|
base.faces("<Z").tag("bot")
|
|
|
|
|
|
|
|
tail = Cq.Solid.makeCylinder(
|
|
|
|
height=self.tail_length,
|
|
|
|
radius=self.thickness / 2)
|
|
|
|
# points cylinder to +X
|
|
|
|
dy = self.radius - self.thickness / 2
|
|
|
|
if self.right_handed:
|
|
|
|
dy = -dy
|
|
|
|
loc_dir_x = Cq.Location((0, 0, self.thickness / 2), (0, 1, 0), 90)
|
|
|
|
loc_shift = Cq.Location((0, dy, 0))
|
|
|
|
loc_top = Cq.Location((0, 0, self.height - self.thickness), (0, 0, 1), omega + 180)
|
|
|
|
result = (
|
|
|
|
base
|
|
|
|
.cylinder(
|
|
|
|
height=self.height,
|
|
|
|
radius=self.radius - self.thickness,
|
|
|
|
combine='s',
|
|
|
|
centered=(True, True, True))
|
|
|
|
.union(tail.located(loc_shift * loc_dir_x))
|
|
|
|
.union(tail.located(loc_top * loc_shift.inverse * loc_dir_x))
|
|
|
|
.clean()
|
|
|
|
)
|
|
|
|
r = -self.radius if self.right_handed else self.radius
|
|
|
|
plane = result.copyWorkplane(Cq.Workplane('XY'))
|
|
|
|
plane.polyline([(0, r, 0), (self.tail_length, r, 0)],
|
|
|
|
forConstruction=True).tag("dir_bot")
|
|
|
|
omega = math.radians(omega)
|
|
|
|
c, s = math.cos(omega), math.sin(omega)
|
|
|
|
l = -self.tail_length
|
|
|
|
plane.polyline([
|
|
|
|
(-s * r, c * r, self.height),
|
|
|
|
(c * l - s * r, c * r + s * l, self.height)],
|
|
|
|
forConstruction=True).tag("dir_top")
|
|
|
|
return result
|