cosplay: Touhou/Houjuu Nue #4
|
@ -43,7 +43,7 @@ ROLE_COLOR_MAP = {
|
||||||
Role.PARENT: _color('blue4', 0.6),
|
Role.PARENT: _color('blue4', 0.6),
|
||||||
Role.CASING: _color('dodgerblue3', 0.6),
|
Role.CASING: _color('dodgerblue3', 0.6),
|
||||||
Role.CHILD: _color('darkorange2', 0.6),
|
Role.CHILD: _color('darkorange2', 0.6),
|
||||||
Role.DAMPING: _color('springgreen', 0.8),
|
Role.DAMPING: _color('springgreen', 1.0),
|
||||||
Role.STRUCTURE: _color('gray', 0.4),
|
Role.STRUCTURE: _color('gray', 0.4),
|
||||||
Role.DECORATION: _color('lightseagreen', 0.4),
|
Role.DECORATION: _color('lightseagreen', 0.4),
|
||||||
Role.ELECTRONIC: _color('mediumorchid', 0.5),
|
Role.ELECTRONIC: _color('mediumorchid', 0.5),
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import math
|
import math
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
import nhf.parts.springs as springs
|
from nhf.parts.springs import TorsionSpring
|
||||||
from nhf import Role
|
from nhf import Role
|
||||||
import nhf.utils
|
import nhf.utils
|
||||||
|
|
||||||
|
@ -158,99 +158,6 @@ class HirthJoint:
|
||||||
offset=offset)
|
offset=offset)
|
||||||
return result.solve()
|
return result.solve()
|
||||||
|
|
||||||
def comma_joint(radius=30,
|
|
||||||
shaft_radius=10,
|
|
||||||
height=10,
|
|
||||||
flange=10,
|
|
||||||
flange_thickness=25,
|
|
||||||
n_serration=16,
|
|
||||||
serration_angle_offset=0,
|
|
||||||
serration_height=5,
|
|
||||||
serration_inner_radius=20,
|
|
||||||
serration_theta=2 * math.pi / 48,
|
|
||||||
serration_tilt=-30,
|
|
||||||
right_handed=False):
|
|
||||||
"""
|
|
||||||
Produces a "o_" shaped joint, with serrations to accomodate a torsion spring
|
|
||||||
"""
|
|
||||||
assert flange_thickness <= radius
|
|
||||||
flange_poly = [
|
|
||||||
(0, radius - flange_thickness),
|
|
||||||
(0, radius),
|
|
||||||
(flange + radius, radius),
|
|
||||||
(flange + radius, radius - flange_thickness)
|
|
||||||
]
|
|
||||||
if right_handed:
|
|
||||||
flange_poly = [(x, -y) for x,y in flange_poly]
|
|
||||||
sketch = (
|
|
||||||
Cq.Sketch()
|
|
||||||
.circle(radius)
|
|
||||||
.polygon(flange_poly, mode='a')
|
|
||||||
.circle(shaft_radius, mode='s')
|
|
||||||
)
|
|
||||||
serration_poly = [
|
|
||||||
(0, 0), (radius, 0),
|
|
||||||
(radius, radius * math.tan(serration_theta))
|
|
||||||
]
|
|
||||||
serration = (
|
|
||||||
Cq.Workplane('XY')
|
|
||||||
.sketch()
|
|
||||||
.polygon(serration_poly)
|
|
||||||
.circle(radius, mode='i')
|
|
||||||
.circle(serration_inner_radius, mode='s')
|
|
||||||
.finalize()
|
|
||||||
.extrude(serration_height)
|
|
||||||
.translate(Cq.Vector((-serration_inner_radius, 0, height)))
|
|
||||||
.rotate(
|
|
||||||
axisStartPoint=(0, 0, 0),
|
|
||||||
axisEndPoint=(0, 0, height),
|
|
||||||
angleDegrees=serration_tilt)
|
|
||||||
.val()
|
|
||||||
)
|
|
||||||
serrations = (
|
|
||||||
Cq.Workplane('XY')
|
|
||||||
.polarArray(radius=serration_inner_radius,
|
|
||||||
startAngle=0+serration_angle_offset,
|
|
||||||
angle=360+serration_angle_offset,
|
|
||||||
count=n_serration)
|
|
||||||
.eachpoint(lambda loc: serration.located(loc))
|
|
||||||
)
|
|
||||||
result = (
|
|
||||||
Cq.Workplane()
|
|
||||||
.add(sketch)
|
|
||||||
.extrude(height)
|
|
||||||
.union(serrations)
|
|
||||||
.clean()
|
|
||||||
)
|
|
||||||
|
|
||||||
result.polyline([
|
|
||||||
(0, 0, height - serration_height),
|
|
||||||
(0, 0, height + serration_height)],
|
|
||||||
forConstruction=True).tag("serrated")
|
|
||||||
result.polyline([
|
|
||||||
(0, radius, 0),
|
|
||||||
(flange + radius, radius, 0)],
|
|
||||||
forConstruction=True).tag("tail")
|
|
||||||
result.faces('>X').tag("tail_end")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def comma_assembly():
|
|
||||||
joint1 = comma_joint()
|
|
||||||
joint2 = comma_joint()
|
|
||||||
spring = springs.torsion_spring()
|
|
||||||
result = (
|
|
||||||
Cq.Assembly()
|
|
||||||
.add(joint1, name="joint1", color=Cq.Color(0.8,0.8,0.5,0.3))
|
|
||||||
.add(joint2, name="joint2", color=Cq.Color(0.8,0.8,0.5,0.3))
|
|
||||||
.add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1))
|
|
||||||
.constrain("joint1?serrated", "spring?bot", "Plane")
|
|
||||||
.constrain("joint2?serrated", "spring?top", "Plane")
|
|
||||||
.constrain("joint1?tail", "FixedAxis", (1, 0, 0))
|
|
||||||
.constrain("joint2?tail", "FixedAxis", (-1, 0, 0))
|
|
||||||
.solve()
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TorsionJoint:
|
class TorsionJoint:
|
||||||
"""
|
"""
|
||||||
|
@ -268,6 +175,13 @@ class TorsionJoint:
|
||||||
2. A slotted annular extrusion where the slot allows the spring to rest
|
2. A slotted annular extrusion where the slot allows the spring to rest
|
||||||
3. An outer and an inner annuli which forms a track the rider can move on
|
3. An outer and an inner annuli which forms a track the rider can move on
|
||||||
"""
|
"""
|
||||||
|
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
|
||||||
|
radius=10.0,
|
||||||
|
thickness=2.0,
|
||||||
|
height=15.0,
|
||||||
|
tail_length=35.0,
|
||||||
|
right_handed=False,
|
||||||
|
))
|
||||||
|
|
||||||
# Radius limit for rotating components
|
# Radius limit for rotating components
|
||||||
radius_track: float = 40
|
radius_track: float = 40
|
||||||
|
@ -275,7 +189,6 @@ class TorsionJoint:
|
||||||
track_disk_height: float = 10
|
track_disk_height: float = 10
|
||||||
rider_disk_height: float = 8
|
rider_disk_height: float = 8
|
||||||
|
|
||||||
radius_spring: float = 15
|
|
||||||
radius_axle: float = 6
|
radius_axle: float = 6
|
||||||
|
|
||||||
# If true, cover the spring hole. May make it difficult to insert the spring
|
# If true, cover the spring hole. May make it difficult to insert the spring
|
||||||
|
@ -283,12 +196,6 @@ class TorsionJoint:
|
||||||
spring_hole_cover_track: bool = False
|
spring_hole_cover_track: bool = False
|
||||||
spring_hole_cover_rider: bool = False
|
spring_hole_cover_rider: bool = False
|
||||||
|
|
||||||
# Also used for the height of the hole for the spring
|
|
||||||
spring_thickness: float = 2
|
|
||||||
spring_height: float = 15
|
|
||||||
|
|
||||||
spring_tail_length: float = 35
|
|
||||||
|
|
||||||
groove_radius_outer: float = 35
|
groove_radius_outer: float = 35
|
||||||
groove_radius_inner: float = 20
|
groove_radius_inner: float = 20
|
||||||
# Gap on inner groove to ease movement
|
# Gap on inner groove to ease movement
|
||||||
|
@ -301,23 +208,19 @@ class TorsionJoint:
|
||||||
rider_slot_begin: float = 0
|
rider_slot_begin: float = 0
|
||||||
rider_slot_span: float = 90
|
rider_slot_span: float = 90
|
||||||
|
|
||||||
right_handed: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
assert self.radius_track > self.groove_radius_outer
|
assert self.radius_track > self.groove_radius_outer
|
||||||
assert self.radius_rider > self.groove_radius_outer
|
assert self.radius_rider > self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap
|
||||||
assert self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap
|
assert self.groove_radius_inner > self.spring.radius > self.radius_axle
|
||||||
assert self.groove_radius_inner > self.radius_spring
|
assert self.spring.height > self.groove_depth, "Groove is too deep"
|
||||||
assert self.spring_height > self.groove_depth, "Groove is too deep"
|
|
||||||
assert self.radius_spring > self.radius_axle
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_height(self):
|
def total_height(self):
|
||||||
"""
|
"""
|
||||||
Total height counting from bottom to top
|
Total height counting from bottom to top
|
||||||
"""
|
"""
|
||||||
return self.track_disk_height + self.rider_disk_height + self.spring_height
|
return self.track_disk_height + self.rider_disk_height + self.spring.height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def radius(self):
|
def radius(self):
|
||||||
|
@ -326,28 +229,24 @@ class TorsionJoint:
|
||||||
"""
|
"""
|
||||||
return max(self.radius_rider, self.radius_track)
|
return max(self.radius_rider, self.radius_track)
|
||||||
|
|
||||||
@property
|
|
||||||
def _radius_spring_internal(self):
|
|
||||||
return self.radius_spring - self.spring_thickness
|
|
||||||
|
|
||||||
def _slot_polygon(self, flip: bool=False):
|
def _slot_polygon(self, flip: bool=False):
|
||||||
r1 = self.radius_spring - self.spring_thickness
|
r1 = self.spring.radius_inner
|
||||||
r2 = self.radius_spring
|
r2 = self.spring.radius
|
||||||
flip = flip != self.right_handed
|
flip = flip != self.spring.right_handed
|
||||||
if flip:
|
if flip:
|
||||||
r1 = -r1
|
r1 = -r1
|
||||||
r2 = -r2
|
r2 = -r2
|
||||||
return [
|
return [
|
||||||
(0, r2),
|
(0, r2),
|
||||||
(self.spring_tail_length, r2),
|
(self.spring.tail_length, r2),
|
||||||
(self.spring_tail_length, r1),
|
(self.spring.tail_length, r1),
|
||||||
(0, r1),
|
(0, r1),
|
||||||
]
|
]
|
||||||
def _directrix(self, height, theta=0):
|
def _directrix(self, height, theta=0):
|
||||||
c, s = math.cos(theta), math.sin(theta)
|
c, s = math.cos(theta), math.sin(theta)
|
||||||
r2 = self.radius_spring
|
r2 = self.spring.radius
|
||||||
l = self.spring_tail_length
|
l = self.spring.tail_length
|
||||||
if self.right_handed:
|
if self.spring.right_handed:
|
||||||
r2 = -r2
|
r2 = -r2
|
||||||
# This is (0, r2) and (l, r2) transformed by right handed rotation
|
# This is (0, r2) and (l, r2) transformed by right handed rotation
|
||||||
# matrix `[[c, -s], [s, c]]`
|
# matrix `[[c, -s], [s, c]]`
|
||||||
|
@ -356,16 +255,6 @@ class TorsionJoint:
|
||||||
(c * l - s * r2, s * l + c * r2, height),
|
(c * l - s * r2, s * l + c * r2, height),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
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=self.right_handed,
|
|
||||||
)
|
|
||||||
|
|
||||||
def track(self):
|
def track(self):
|
||||||
# TODO: Cover outer part of track only. Can we do this?
|
# TODO: Cover outer part of track only. Can we do this?
|
||||||
groove_profile = (
|
groove_profile = (
|
||||||
|
@ -373,14 +262,14 @@ class TorsionJoint:
|
||||||
.circle(self.radius_track)
|
.circle(self.radius_track)
|
||||||
.circle(self.groove_radius_outer, mode='s')
|
.circle(self.groove_radius_outer, mode='s')
|
||||||
.circle(self.groove_radius_inner, mode='a')
|
.circle(self.groove_radius_inner, mode='a')
|
||||||
.circle(self.radius_spring, mode='s')
|
.circle(self.spring.radius, mode='s')
|
||||||
)
|
)
|
||||||
spring_hole_profile = (
|
spring_hole_profile = (
|
||||||
Cq.Sketch()
|
Cq.Sketch()
|
||||||
.circle(self.radius_track)
|
.circle(self.radius_track)
|
||||||
.circle(self.radius_spring, mode='s')
|
.circle(self.spring.radius, mode='s')
|
||||||
)
|
)
|
||||||
slot_height = self.spring_thickness
|
slot_height = self.spring.thickness
|
||||||
if not self.spring_hole_cover_track:
|
if not self.spring_hole_cover_track:
|
||||||
slot_height += self.groove_depth
|
slot_height += self.groove_depth
|
||||||
slot = (
|
slot = (
|
||||||
|
@ -400,7 +289,7 @@ class TorsionJoint:
|
||||||
.faces('>Z')
|
.faces('>Z')
|
||||||
.tag("spring")
|
.tag("spring")
|
||||||
.placeSketch(spring_hole_profile)
|
.placeSketch(spring_hole_profile)
|
||||||
.extrude(self.spring_thickness)
|
.extrude(self.spring.thickness)
|
||||||
# If the spring hole profile is not simply connected, this workplane
|
# If the spring hole profile is not simply connected, this workplane
|
||||||
# will have to be created from the `spring-mate` face.
|
# will have to be created from the `spring-mate` face.
|
||||||
.faces('>Z')
|
.faces('>Z')
|
||||||
|
@ -425,7 +314,7 @@ class TorsionJoint:
|
||||||
wall_profile = (
|
wall_profile = (
|
||||||
Cq.Sketch()
|
Cq.Sketch()
|
||||||
.circle(self.radius_rider, mode='a')
|
.circle(self.radius_rider, mode='a')
|
||||||
.circle(self.radius_spring, mode='s')
|
.circle(self.spring.radius, mode='s')
|
||||||
.parray(
|
.parray(
|
||||||
r=0,
|
r=0,
|
||||||
a1=rider_slot_begin,
|
a1=rider_slot_begin,
|
||||||
|
@ -451,7 +340,7 @@ class TorsionJoint:
|
||||||
.reset()
|
.reset()
|
||||||
)
|
)
|
||||||
#.circle(self._radius_wall, mode='a')
|
#.circle(self._radius_wall, mode='a')
|
||||||
middle_height = self.spring_height - self.groove_depth - self.rider_gap - self.spring_thickness
|
middle_height = self.spring.height - self.groove_depth - self.rider_gap - self.spring.thickness
|
||||||
result = (
|
result = (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.cylinder(
|
.cylinder(
|
||||||
|
@ -471,8 +360,8 @@ class TorsionJoint:
|
||||||
.extrude(self.groove_depth + self.rider_gap)
|
.extrude(self.groove_depth + self.rider_gap)
|
||||||
.faces(tag="spring")
|
.faces(tag="spring")
|
||||||
.workplane()
|
.workplane()
|
||||||
.circle(self._radius_spring_internal)
|
.circle(self.spring.radius_inner)
|
||||||
.extrude(self.spring_height)
|
.extrude(self.spring.height)
|
||||||
.faces("<Z")
|
.faces("<Z")
|
||||||
.workplane()
|
.workplane()
|
||||||
.hole(self.radius_axle * 2)
|
.hole(self.radius_axle * 2)
|
||||||
|
@ -490,10 +379,10 @@ class TorsionJoint:
|
||||||
forConstruction=True).tag(f"dir{j}")
|
forConstruction=True).tag(f"dir{j}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rider_track_assembly(self, directrix=0):
|
def rider_track_assembly(self, directrix: int = 0, deflection: float = 0):
|
||||||
rider = self.rider()
|
rider = self.rider()
|
||||||
track = self.track()
|
track = self.track()
|
||||||
spring = self.spring()
|
spring = self.spring.generate(deflection=deflection)
|
||||||
result = (
|
result = (
|
||||||
Cq.Assembly()
|
Cq.Assembly()
|
||||||
.addS(spring, name="spring", role=Role.DAMPING)
|
.addS(spring, name="spring", role=Role.DAMPING)
|
||||||
|
|
|
@ -1,59 +1,75 @@
|
||||||
import math
|
import math
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
|
|
||||||
def torsion_spring(radius=12,
|
@dataclass
|
||||||
height=20,
|
class TorsionSpring:
|
||||||
thickness=2,
|
|
||||||
omega=90,
|
|
||||||
tail_length=25,
|
|
||||||
right_handed: bool = False):
|
|
||||||
"""
|
"""
|
||||||
Produces a torsion spring with abridged geometry since sweep is very slow in
|
A torsion spring with abridged geometry (since sweep is slow)
|
||||||
cq-editor.
|
|
||||||
"""
|
"""
|
||||||
if right_handed:
|
# Outer radius
|
||||||
omega = -omega
|
radius: float = 12.0
|
||||||
base = (
|
height: float = 20.0
|
||||||
Cq.Workplane('XY')
|
thickness: float = 2.0
|
||||||
.cylinder(height=height, radius=radius,
|
|
||||||
centered=(True, True, False))
|
|
||||||
)
|
|
||||||
base.faces(">Z").tag("top")
|
|
||||||
base.faces("<Z").tag("bot")
|
|
||||||
|
|
||||||
box_shift = -radius if right_handed else radius-thickness
|
# Angle (in degrees) between the two legs at neutral position
|
||||||
result = (
|
angle_neutral: float = 90.0
|
||||||
base
|
|
||||||
.cylinder(height=height, radius=radius - thickness, combine='s',
|
tail_length: float = 25.0
|
||||||
centered=(True, True, True))
|
right_handed: bool = False
|
||||||
.transformed(
|
|
||||||
offset=(0, box_shift),
|
torsion_rate: Optional[float] = None
|
||||||
rotate=(0, 0, 0))
|
|
||||||
.box(
|
@property
|
||||||
length=tail_length,
|
def radius_inner(self) -> float:
|
||||||
width=thickness,
|
return self.radius - self.thickness
|
||||||
height=thickness,
|
|
||||||
centered=False)
|
def torque_at(self, theta: float) -> float:
|
||||||
.copyWorkplane(Cq.Workplane('XY'))
|
return self.torsion_rate * theta
|
||||||
.transformed(
|
|
||||||
offset=(0, 0, height - thickness),
|
def generate(self, deflection: float = 0):
|
||||||
rotate=(0, 0, omega))
|
omega = self.angle_neutral + deflection
|
||||||
.center(-tail_length, box_shift)
|
omega = -omega if self.right_handed else omega
|
||||||
.box(
|
base = (
|
||||||
length=tail_length,
|
Cq.Workplane('XY')
|
||||||
width=thickness,
|
.cylinder(height=self.height, radius=self.radius,
|
||||||
height=thickness,
|
centered=(True, True, False))
|
||||||
centered=False)
|
)
|
||||||
)
|
base.faces(">Z").tag("top")
|
||||||
r = -radius if right_handed else radius
|
base.faces("<Z").tag("bot")
|
||||||
plane = result.copyWorkplane(Cq.Workplane('XY'))
|
|
||||||
plane.polyline([(0, r, 0), (tail_length, r, 0)],
|
box_shift = -self.radius if self.right_handed else self.radius-self.thickness
|
||||||
forConstruction=True).tag("dir_bot")
|
tail = Cq.Solid.makeCylinder(
|
||||||
omega = math.radians(omega)
|
height=self.tail_length,
|
||||||
c, s = math.cos(omega), math.sin(omega)
|
radius=self.thickness / 2)
|
||||||
l = -tail_length
|
# points cylinder to +X
|
||||||
plane.polyline([
|
dy = self.radius - self.thickness / 2
|
||||||
(-s * r, c * r, height),
|
if self.right_handed:
|
||||||
(c * l - s * r, c * r + s * l, height)],
|
dy = -dy
|
||||||
forConstruction=True).tag("dir_top")
|
loc_dir_x = Cq.Location((0, 0, self.thickness / 2), (0, 1, 0), 90)
|
||||||
return result
|
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
|
||||||
|
|
|
@ -20,8 +20,6 @@ class TestJoints(unittest.TestCase):
|
||||||
isect = binary_intersection(assembly)
|
isect = binary_intersection(assembly)
|
||||||
self.assertLess(isect.Volume(), 1e-6,
|
self.assertLess(isect.Volume(), 1e-6,
|
||||||
"Hirth joint assembly must not have intersection")
|
"Hirth joint assembly must not have intersection")
|
||||||
def test_joints_comma_assembly(self):
|
|
||||||
joints.comma_assembly()
|
|
||||||
|
|
||||||
def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int):
|
def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int):
|
||||||
assert 0 <= slot and slot < joint.rider_n_slots
|
assert 0 <= slot and slot < joint.rider_n_slots
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import math
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf import Material, Role
|
from nhf import Material, Role
|
||||||
from nhf.build import Model, target, assembly
|
from nhf.build import Model, target, assembly
|
||||||
import nhf.parts.springs as springs
|
from nhf.parts.springs import TorsionSpring
|
||||||
from nhf.parts.joints import TorsionJoint
|
from nhf.parts.joints import TorsionJoint
|
||||||
from nhf.parts.box import box_with_centre_holes
|
from nhf.parts.box import box_with_centre_holes
|
||||||
import nhf.utils
|
import nhf.utils
|
||||||
|
@ -23,10 +24,12 @@ class ShoulderJoint(Model):
|
||||||
rider_disk_height=5.0,
|
rider_disk_height=5.0,
|
||||||
# M8 Axle
|
# M8 Axle
|
||||||
radius_axle=3.0,
|
radius_axle=3.0,
|
||||||
# inner diameter = 9
|
spring=TorsionSpring(
|
||||||
radius_spring=9/2 + 1.2,
|
# inner diameter = 9
|
||||||
spring_thickness=1.3,
|
radius=9/2 + 1.2,
|
||||||
spring_height=7.5,
|
thickness=1.3,
|
||||||
|
height=7.5,
|
||||||
|
),
|
||||||
))
|
))
|
||||||
|
|
||||||
# On the parent side, drill vertical holes
|
# On the parent side, drill vertical holes
|
||||||
|
@ -189,12 +192,12 @@ class ShoulderJoint(Model):
|
||||||
.addS(self.child(), name="child",
|
.addS(self.child(), name="child",
|
||||||
role=Role.CHILD, material=mat)
|
role=Role.CHILD, material=mat)
|
||||||
.constrain("child/core", "Fixed")
|
.constrain("child/core", "Fixed")
|
||||||
.addS(self.torsion_joint.spring(), name="spring_top",
|
.addS(self.torsion_joint.spring.generate(), name="spring_top",
|
||||||
role=Role.DAMPING, material=mat_spring)
|
role=Role.DAMPING, material=mat_spring)
|
||||||
.addS(self.parent(wing_root_wall_thickness),
|
.addS(self.parent(wing_root_wall_thickness),
|
||||||
name="parent_top",
|
name="parent_top",
|
||||||
role=Role.PARENT, material=mat)
|
role=Role.PARENT, material=mat)
|
||||||
.addS(self.torsion_joint.spring(), name="spring_bot",
|
.addS(self.torsion_joint.spring.generate(), name="spring_bot",
|
||||||
role=Role.DAMPING, material=mat_spring)
|
role=Role.DAMPING, material=mat_spring)
|
||||||
.addS(self.parent(wing_root_wall_thickness),
|
.addS(self.parent(wing_root_wall_thickness),
|
||||||
name="parent_bot",
|
name="parent_bot",
|
||||||
|
@ -280,25 +283,27 @@ class DiskJoint(Model):
|
||||||
"""
|
"""
|
||||||
Sandwiched disk joint for the wrist and elbow
|
Sandwiched disk joint for the wrist and elbow
|
||||||
"""
|
"""
|
||||||
|
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
|
||||||
|
radius=9 / 2,
|
||||||
|
thickness=1.3,
|
||||||
|
height=6.5,
|
||||||
|
tail_length=45.0,
|
||||||
|
right_handed=False,
|
||||||
|
))
|
||||||
|
|
||||||
radius_housing: float = 22.0
|
radius_housing: float = 22.0
|
||||||
radius_disk: float = 20.0
|
radius_disk: float = 20.0
|
||||||
radius_spring: float = 9 / 2
|
|
||||||
radius_axle: float = 3.0
|
radius_axle: float = 3.0
|
||||||
|
|
||||||
housing_thickness: float = 5.0
|
housing_thickness: float = 5.0
|
||||||
disk_thickness: float = 5.0
|
disk_thickness: float = 5.0
|
||||||
# Gap between disk and the housing
|
# Gap between disk and the housing
|
||||||
#disk_thickness_gap: float = 0.1
|
#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 at 0 degrees of movement
|
||||||
spring_angle: float = 30.0
|
spring_angle_at_0: float = 30.0
|
||||||
# Angle at which the spring exerts no torque
|
spring_slot_offset: float = 15.0
|
||||||
spring_angle_neutral: float = 90.0
|
|
||||||
spring_angle_shift: float = 30
|
|
||||||
wall_inset: float = 2.0
|
wall_inset: float = 2.0
|
||||||
|
|
||||||
# Angular span of movement
|
# Angular span of movement
|
||||||
|
@ -317,18 +322,9 @@ class DiskJoint(Model):
|
||||||
assert self.radius_disk > self.radius_axle
|
assert self.radius_disk > self.radius_axle
|
||||||
assert self.housing_upper_carve_offset > 0
|
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
|
@property
|
||||||
def neutral_movement_angle(self) -> Optional[float]:
|
def neutral_movement_angle(self) -> Optional[float]:
|
||||||
a = self.spring_angle_neutral - self.spring_angle
|
a = self.spring.angle_neutral - self.spring_angle_at_0
|
||||||
if 0 <= a and a <= self.movement_angle:
|
if 0 <= a and a <= self.movement_angle:
|
||||||
return a
|
return a
|
||||||
return None
|
return None
|
||||||
|
@ -346,7 +342,7 @@ class DiskJoint(Model):
|
||||||
"""
|
"""
|
||||||
Distance between the spring track and the outside of the upper housing
|
Distance between the spring track and the outside of the upper housing
|
||||||
"""
|
"""
|
||||||
return self.housing_thickness + self.disk_thickness - self.spring_height
|
return self.housing_thickness + self.disk_thickness - self.spring.height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def housing_upper_dz(self) -> float:
|
def housing_upper_dz(self) -> float:
|
||||||
|
@ -355,42 +351,17 @@ class DiskJoint(Model):
|
||||||
"""
|
"""
|
||||||
return self.total_thickness / 2 - self.housing_thickness
|
return self.total_thickness / 2 - self.housing_thickness
|
||||||
|
|
||||||
@property
|
|
||||||
def radius_spring_internal(self):
|
|
||||||
return self.radius_spring - self.spring_thickness
|
|
||||||
|
|
||||||
@target(name="disk")
|
@target(name="disk")
|
||||||
def disk(self) -> Cq.Workplane:
|
def disk(self) -> Cq.Workplane:
|
||||||
cut = (
|
cut = (
|
||||||
Cq.Solid.makeBox(
|
Cq.Solid.makeBox(
|
||||||
length=self.spring_tail_length,
|
length=self.spring.tail_length,
|
||||||
width=self.spring_thickness,
|
width=self.spring.thickness,
|
||||||
height=self.disk_thickness,
|
height=self.disk_thickness,
|
||||||
)
|
)
|
||||||
.located(Cq.Location((0, self.radius_spring_internal, 0)))
|
.located(Cq.Location((0, self.spring.radius_inner, 0)))
|
||||||
.rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift)
|
.rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset)
|
||||||
)
|
)
|
||||||
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
|
radius_tongue = self.radius_disk + self.tongue_length
|
||||||
tongue = (
|
tongue = (
|
||||||
Cq.Solid.makeCylinder(
|
Cq.Solid.makeCylinder(
|
||||||
|
@ -402,7 +373,29 @@ class DiskJoint(Model):
|
||||||
radius=self.radius_disk,
|
radius=self.radius_disk,
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
result = result.union(tongue, tol=TOL)
|
result = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.cylinder(
|
||||||
|
height=self.disk_thickness,
|
||||||
|
radius=self.radius_disk,
|
||||||
|
centered=(True, True, False)
|
||||||
|
)
|
||||||
|
.union(tongue, tol=TOL)
|
||||||
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
|
.cylinder(
|
||||||
|
height=self.disk_thickness,
|
||||||
|
radius=self.spring.radius,
|
||||||
|
centered=(True, True, False),
|
||||||
|
combine='cut',
|
||||||
|
)
|
||||||
|
.cut(cut)
|
||||||
|
)
|
||||||
|
plane = result.copyWorkplane(Cq.Workplane('XY'))
|
||||||
|
theta = math.radians(self.spring_slot_offset)
|
||||||
|
plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0))
|
||||||
|
plane.workplane(offset=self.disk_thickness).tagPlane("mate_top")
|
||||||
|
result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def wall(self) -> Cq.Compound:
|
def wall(self) -> Cq.Compound:
|
||||||
|
@ -433,9 +426,6 @@ class DiskJoint(Model):
|
||||||
)
|
)
|
||||||
result.faces(">Z").tag("mate")
|
result.faces(">Z").tag("mate")
|
||||||
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
|
result.faces(">Z").workplane().tagPlane("dirX", direction="+X")
|
||||||
# two directional vectors are required to make the angle constrain
|
|
||||||
# unambiguous
|
|
||||||
result.faces(">Z").workplane().tagPlane("dirY", direction="+Y")
|
|
||||||
result = result.cut(
|
result = result.cut(
|
||||||
self
|
self
|
||||||
.wall()
|
.wall()
|
||||||
|
@ -447,16 +437,17 @@ class DiskJoint(Model):
|
||||||
|
|
||||||
@target(name="housing-upper")
|
@target(name="housing-upper")
|
||||||
def housing_upper(self) -> Cq.Workplane:
|
def housing_upper(self) -> Cq.Workplane:
|
||||||
|
carve_angle = -(self.spring_angle_at_0 - self.spring_slot_offset)
|
||||||
carve = (
|
carve = (
|
||||||
Cq.Solid.makeCylinder(
|
Cq.Solid.makeCylinder(
|
||||||
radius=self.radius_spring,
|
radius=self.spring.radius,
|
||||||
height=self.housing_thickness
|
height=self.housing_thickness
|
||||||
).fuse(Cq.Solid.makeBox(
|
).fuse(Cq.Solid.makeBox(
|
||||||
length=self.spring_tail_length,
|
length=self.spring.tail_length,
|
||||||
width=self.spring_thickness,
|
width=self.spring.thickness,
|
||||||
height=self.housing_thickness
|
height=self.housing_thickness
|
||||||
).located(Cq.Location((0, -self.radius_spring, 0))))
|
).located(Cq.Location((0, -self.spring.radius, 0))))
|
||||||
).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift)
|
).rotate((0, 0, 0), (0, 0, 1), carve_angle)
|
||||||
result = (
|
result = (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.cylinder(
|
.cylinder(
|
||||||
|
@ -465,8 +456,11 @@ class DiskJoint(Model):
|
||||||
centered=(True, True, False),
|
centered=(True, True, False),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
theta = math.radians(carve_angle)
|
||||||
result.faces("<Z").tag("mate")
|
result.faces("<Z").tag("mate")
|
||||||
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
|
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))
|
||||||
result = result.faces(">Z").hole(self.radius_axle * 2)
|
result = result.faces(">Z").hole(self.radius_axle * 2)
|
||||||
|
|
||||||
# tube which holds the spring interior
|
# tube which holds the spring interior
|
||||||
|
@ -490,28 +484,42 @@ class DiskJoint(Model):
|
||||||
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
|
.cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset))))
|
||||||
.union(wall, tol=TOL)
|
.union(wall, tol=TOL)
|
||||||
)
|
)
|
||||||
return result
|
return result.clean()
|
||||||
|
|
||||||
def add_constraints(self,
|
def add_constraints(self,
|
||||||
assembly: Cq.Assembly,
|
assembly: Cq.Assembly,
|
||||||
housing_lower: str,
|
housing_lower: str,
|
||||||
housing_upper: str,
|
housing_upper: str,
|
||||||
disk: str,
|
disk: str,
|
||||||
angle: float,
|
angle: float = 0.0,
|
||||||
) -> Cq.Assembly:
|
) -> Cq.Assembly:
|
||||||
return (
|
deflection = angle - self.neutral_movement_angle
|
||||||
|
spring_name = disk.replace("/", "__Z") + "_spring"
|
||||||
|
(
|
||||||
assembly
|
assembly
|
||||||
|
.addS(
|
||||||
|
self.spring.generate(deflection=-deflection),
|
||||||
|
name=spring_name,
|
||||||
|
role=Role.DAMPING,
|
||||||
|
material=Material.STEEL_SPRING)
|
||||||
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
|
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
|
||||||
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
|
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
|
||||||
.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0)
|
.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0)
|
||||||
.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle)
|
.constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0)
|
||||||
.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90)
|
.constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0)
|
||||||
|
.constrain(f"{disk}?mate_bot", f"{spring_name}?bot", "Plane", param=0)
|
||||||
|
#.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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
|
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
|
||||||
if angle is None:
|
if angle is None:
|
||||||
angle = self.movement_angle
|
angle = self.neutral_movement_angle
|
||||||
if angle is None:
|
if angle is None:
|
||||||
angle = 0
|
angle = 0
|
||||||
else:
|
else:
|
||||||
|
@ -521,7 +529,7 @@ class DiskJoint(Model):
|
||||||
.addS(self.disk(), name="disk", role=Role.CHILD)
|
.addS(self.disk(), name="disk", role=Role.CHILD)
|
||||||
.addS(self.housing_lower(), name="housing_lower", role=Role.PARENT)
|
.addS(self.housing_lower(), name="housing_lower", role=Role.PARENT)
|
||||||
.addS(self.housing_upper(), name="housing_upper", role=Role.CASING)
|
.addS(self.housing_upper(), name="housing_upper", role=Role.CASING)
|
||||||
#.constrain("housing_lower", "Fixed")
|
.constrain("housing_lower", "Fixed")
|
||||||
)
|
)
|
||||||
result = self.add_constraints(
|
result = self.add_constraints(
|
||||||
result,
|
result,
|
||||||
|
@ -627,10 +635,15 @@ class ElbowJoint:
|
||||||
.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2)
|
.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2)
|
||||||
)
|
)
|
||||||
housing = self.disk_joint.housing_upper()
|
housing = self.disk_joint.housing_upper()
|
||||||
|
housing_loc = Cq.Location(
|
||||||
|
(0, 0, housing_dz),
|
||||||
|
(0, 0, 1),
|
||||||
|
-self.disk_joint.tongue_span / 2
|
||||||
|
)
|
||||||
result = (
|
result = (
|
||||||
self.parent_beam.beam()
|
self.parent_beam.beam()
|
||||||
.add(housing, name="housing",
|
.add(housing, name="housing",
|
||||||
loc=axial_offset * Cq.Location((0, 0, housing_dz)))
|
loc=axial_offset * housing_loc)
|
||||||
.add(connector, name="connector",
|
.add(connector, name="connector",
|
||||||
loc=axial_offset)
|
loc=axial_offset)
|
||||||
#.constrain("housing", "Fixed")
|
#.constrain("housing", "Fixed")
|
||||||
|
|
|
@ -545,7 +545,8 @@ class WingProfile(Model):
|
||||||
return result.solve()
|
return result.solve()
|
||||||
|
|
||||||
def assembly(self,
|
def assembly(self,
|
||||||
parts: Optional[list[str]] = None
|
parts: Optional[list[str]] = None,
|
||||||
|
angle_elbow_wrist: float = 0.0,
|
||||||
) -> Cq.Assembly():
|
) -> Cq.Assembly():
|
||||||
if parts is None:
|
if parts is None:
|
||||||
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
|
parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"]
|
||||||
|
@ -575,7 +576,7 @@ class WingProfile(Model):
|
||||||
.constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane")
|
.constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane")
|
||||||
)
|
)
|
||||||
if "elbow" in parts:
|
if "elbow" in parts:
|
||||||
result.add(self.elbow_joint.assembly(), name="elbow")
|
result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow")
|
||||||
if "s1" in parts and "elbow" in parts:
|
if "s1" in parts and "elbow" in parts:
|
||||||
(
|
(
|
||||||
result
|
result
|
||||||
|
@ -595,24 +596,25 @@ class WingProfile(Model):
|
||||||
.constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane")
|
.constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane")
|
||||||
)
|
)
|
||||||
if "wrist" in parts:
|
if "wrist" in parts:
|
||||||
result.add(self.wrist_joint.assembly(), name="wrist")
|
result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist")
|
||||||
if "s2" in parts and "wrist" in parts:
|
if "s2" in parts and "wrist" in parts:
|
||||||
|
# Mounted backwards to bend in other direction
|
||||||
(
|
(
|
||||||
result
|
result
|
||||||
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane")
|
.constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane")
|
||||||
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane")
|
.constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane")
|
||||||
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane")
|
.constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane")
|
||||||
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane")
|
.constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?conn1", "Plane")
|
||||||
)
|
)
|
||||||
if "s3" in parts:
|
if "s3" in parts:
|
||||||
result.add(self.assembly_s3(), name="s3")
|
result.add(self.assembly_s3(), name="s3")
|
||||||
if "s3" in parts and "wrist" in parts:
|
if "s3" in parts and "wrist" in parts:
|
||||||
(
|
(
|
||||||
result
|
result
|
||||||
.constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane")
|
.constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane")
|
||||||
.constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane")
|
.constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane")
|
||||||
.constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane")
|
.constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane")
|
||||||
.constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane")
|
.constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane")
|
||||||
)
|
)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
result.solve()
|
result.solve()
|
||||||
|
|
Loading…
Reference in New Issue