refactor: Create class for torsion spring

This commit is contained in:
Leni Aniva 2024-07-16 13:28:53 -07:00
parent cdb46263f8
commit ef6b2a8663
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
6 changed files with 201 additions and 283 deletions

View File

@ -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),

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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()