'''
Module exposing many functions to create/visualize standard parts.
Those are following the ISO (metric) specifications as often as possible.
Most parts are as easy to create as:
>>> nut(8) # nut with nominal diameter 8mm
>>> screw(8, 16) # screw with match diameter 8mm, and length 16mm
The parts usually have many optional parameters that default to usual recommended values. You can override this by setting the keyword arguments:
>>> screw(8, 16, head='button', drive='slot')
'''
from operator import itemgetter
from itertools import accumulate
from .mathutils import *
from .primitives import *
from .kinematic import Solid
from .mesh import Mesh, Web, Wire, web, wire
from .generation import *
from .blending import *
from .boolean import pierce, union, difference, intersection
from .cut import *
from .io import cachefunc
#cachefunc = lambda x:x # debug purpose
__all__ = [ 'nut', 'screw', 'washer', 'bolt',
'coilspring_compression', 'coilspring_tension', 'coilspring_torsion',
'bearing', 'slidebearing',
'section_s', 'section_w', 'section_c', 'section_l', 'section_tslot',
'screw_slot', 'bolt_slot', 'bearing_slot_exterior', 'bearing_slot_interior',
'stfloor', 'stceil',
]
# --------- numeric stuff --------------------
standard_digits = [.0, 1., .5, .4, .6, .8, .2, .25, .75]
[docs]def stfloor(x, precision=0.1):
''' Return a numeric value fitting x, with lower digits being the under closest digits from `standard_digits`
`precision` gives the relative tolerance interval for the returned number, so we are sure it lays in `[x*(1-precision), x]`
'''
if hasattr(x, '__getitem__'):
return type(x)(stfloor(e) for e in x)
if not isfinite(x):
return x
s, x = sign(x), abs(x)
base = 10
magnitude = base**floor(log(x) / log(base))
ratio = x / magnitude
for j in range(2 + int(ceil(-log(precision) / log(base)))):
for st in standard_digits:
candidate = floor(ratio) + st
if candidate > ratio + 2*NUMPREC:
continue
if ratio*(1-precision) + NUMPREC <= candidate:
return candidate * magnitude * s
magnitude /= base
ratio *= base
raise RuntimeError('failed to converge, this is a bug')
[docs]def stceil(x, precision=0.1):
''' Return a numeric value fitting x, with lower digits being the above closest digits from `standard_digits`
`precision` gives the relative tolerance interval for the returned number, so we are sure it lays in `[x, x*(1+precision)]`
'''
if hasattr(x, '__getitem__'):
return type(x)(stceil(e) for e in x)
if not isfinite(x):
return x
s, x = sign(x), abs(x)
base = 10
magnitude = base**ceil(log(x) / log(base))
ratio = x / magnitude
for j in range(2 + int(ceil(-log(precision) / log(base)))):
for st in standard_digits:
candidate = floor(ratio) + st
if candidate < ratio - 2*NUMPREC:
continue
if ratio*(1+precision) + NUMPREC >= candidate:
return candidate * magnitude * s
magnitude /= base
ratio *= base
raise RuntimeError('failed to converge, this is a bug')
# --------- screw stuff -----------------------
[docs]@cachefunc
def screw(d, length, filet_length=None, head='SH', drive=None, detail=False):
''' Create a standard screw using the given drive and head shapes
Parameters:
d: nominal diameter of the screw
length: length from the screw head to the tip of the screw
filet_length: length of the filet on the screw (from the tip), defaults to `length`
head: name of the screw head shape
drive: name of the screw drive shape
detail: if True, the thread surface will be generated, else it will only be a cylinder
It's also possible to specify head and drive at once, using their codenames:
>>> screw(5, 16, head='SHT') # socket head and torx drive
Available screw heads:
* socket (default)
* button
* flat (aka. cone)
Available screw drives:
* hex
* torx *(not yet available)*
* phillips (cross) *(not yet available)*
* slot
All possible shapes:
* see wikipedia for the [drive types](https://en.wikipedia.org/wiki/List_of_screw_drives)
* see [here for a guide of what to use](https://www.homestratosphere.com/types-of-screws/)
* see wikipedia for [standard screw thread](https://en.wikipedia.org/wiki/ISO_metric_screw_thread)
'''
if filet_length is None: filet_length = length - 0.05*d
elif length < filet_length: raise ValueError('filet_length must be smaller than length')
head, drive = screw_spec(head, drive)
head = globals()['screwhead_'+head](d)
if drive:
drive = globals()['screwdrive_'+drive](d) .transform(boundingbox(head).max[2]*Z)
head = intersection(head, drive)
r = 0.5*d
axis = Axis(O, Z, interval=(-length,r))
body = revolution(2*pi, axis, wire([
-vec3(r, 0, 0.05*d),
-vec3(r, 0, length-filet_length),
-vec3(r, 0, length-r*0.2),
-vec3(r*0.8, 0, length),
-vec3(0, 0, length),
]) .segmented())
screw = (body + head).finish()
return Solid(part=screw, axis=axis)
def screwdrive_torx(d):
indev
def screwdrive_hex(d):
base = regon((-0.3*d*Z, -Z), 0.5*d, 6)
socket = extrusion(d*Z, base) + blendloop(base, center=-0.6*d*Z, weight=-1)
socket.mergeclose()
return socket
def screwdrive_cross(d):
indev
def screwdrive_slot(d):
w = 0.15*d
h = 0.3*d
e = 2*d
return extrusion(2*e*X, web([
vec3(-e, w, h),
vec3(-e, w, -h),
vec3(-e, -w, -h),
vec3(-e, -w, h),
]) .segmented())
def screwhead_socket(d):
''' Screw head shape for socket head (SH) '''
r = h = 0.7*d
c = 0.05*d
profile = wire([
vec3(d/2, 0, -c),
vec3(d/2+c, 0, 0),
vec3(r, 0, 0),
vec3(r, 0, h-c),
vec3(r-c, 0, h),
vec3(0, 0, h),
]) .segmented()
head = revolution(-2*pi, (O,Z), profile)
head.finish()
return head
def screwhead_hex(d):
''' Screw head shape for hex head (HH) '''
r = 0.9*d
h = 0.6*d
c = 0.05*d
profile = extrusion(2*d*Z, regon((-d*Z,Z), r, 6))
cone = revolution(2*pi, (O,Z), web([
vec3(0, 0, h),
vec3(0.8*r, 0, h),
vec3(1.01*r, 0, h*0.8),
vec3(1.01*r, 0, 0),
vec3(0.5*d+c, 0, 0),
vec3(0.5*d, 0, -c),
]) .segmented())
head = intersection(cone, profile)
head.finish()
return head
def screwhead_button(d):
r = 0.95*d
h = 0.5*d
c = 0.05*d
profile = [
wire([
vec3(0, 0, h),
vec3(0, 0.5*d, h),
]),
TangentEllipsis(
vec3(0, 0.5*d, h),
vec3(0, 0.9*r, h),
vec3(0, r, 0.1*d)),
wire([
vec3(0, r, 0.1*d),
vec3(0, r, 0),
vec3(0, 0.5*d+c, 0),
vec3(0, 0.5*d, -c),
]) .segmented(),
]
return revolution(2*pi, (O,Z), profile)
def screwhead_flat(d):
r = d
h = 0.5*d
e = 0.1*d
return revolution(2*pi, (O,Z), web([
vec3(0, 0, h+e),
vec3(0, r, h+e),
vec3(0, r, h),
vec3(0, 0.5*d, 0),
]) .segmented() )
def screwhead_none(d):
indev
'''
head shapes:
Abbreviation Expansion Comment
BH button head
FH flat head
OH oval head
PH Phillips head
RH round head
FHP flat head Phillips
RHP round head Phillips
SH socket head Although "socket head" could logically refer to almost any female drive, it refers by convention to hex socket head unless further specified.
SS set screw The abbreviation "SS" more often means stainless steel. Therefore, "SS cap screw" means "stainless steel cap screw" but "SHSS" means "socket head set screw". As with many abbreviations, users rely on context to diminish the ambiguity, although this reliance does not eliminate it.
VH conic head
CS cap screw
MS machine screw
BHCS button head cap screw
BHMS button head machine screw
FHCS flat head cap screw
FHSCS flat head socket cap screw
FHPMS flat head Phillips machine screw
FT full thread
HHCS hex head cap screw
HSHCS Hexalobular socket head cap screws
RHMS round head machine screw
RHPMS round head Phillips machine screw
SBHCS socket button head cap screw
SBHMS socket button head machine screw
SHCS socket head cap screw
SHSS socket head set screw Sometimes Socket Head Shoulder Screw.
STS self-tapping screw
[standard screw thread](https://en.wikipedia.org/wiki/ISO_metric_screw_thread)
'''
screwheads_codes = {
'SH': 'socket',
'HH': 'hex',
'VH': 'flat',
'cone': 'flat',
'BH': 'button',
'SBH': 'button',
'SS': 'none',
'FT': 'none',
}
screwdrives_codes = {
'TX': 'torx',
'T': 'torx',
'TR': 'torx',
'CS': 'cap',
'phillips': 'cross',
'P': 'cross',
'PH': 'cross',
'PH': 'cross',
}
def screw_spec(head, drive=None):
if not drive:
if head.isupper():
for code in screwdrives_codes:
if head.endswith(code):
head, drive = head[:-len(code)], code
break
head = screwheads_codes.get(head, head)
drive = screwdrives_codes.get(drive, drive)
# special cases
if head != 'hex' and not drive:
drive = 'hex'
return head, drive
# ------------------- nut stuff ----------------------
[docs]@cachefunc
def nut(d, type='hex', detail=False) -> Mesh:
''' Create a standard nut model using the given shape type
Parameters:
d: nominal diameter of the matching screw
type: the nut shape
detail: if True, the thread surface will be generated, else it will only be a cylinder
If `d` alone is given, the other parameters default to the ISO specs: https://www.engineersedge.com/iso_flat_washer.htm
Currently only shape 'hex' is available.
'''
args = standard_hexnuts[bisect(standard_hexnuts, d, key=itemgetter(0))]
if args[0] != d:
raise ValueError('no standard nut for the given diameter')
return hexnut(*args)
def hexnut(d, w, h):
''' Create an hexagon nut with custom dimensions '''
# revolution profile
w *= 0.5
r = 1.01 * w/cos(radians(30))
profile = wire([
vec3(0.5*d, 0, 0.5*h),
vec3(0.95*w, 0, 0.5*h),
vec3(r, 0, 0.5*h - (r-w)),
vec3(r, 0, -0.5*h + (r-w)),
vec3(0.95*w, 0, -0.5*h),
vec3(0.5*d, 0, -0.5*h),
]) .close() .segmented()
base = revolution(2*pi, Axis(O,Z), web(profile))
# exterior hexagon shape
hexagon = regon((-h*Z,Z), w/cos(radians(30)), 6)
ext = extrusion(2*h*Z, hexagon)
# intersect everything
nut = intersection(base, ext)
chamfer(nut, nut.frontiers(4,5) + nut.frontiers(0,5), ('width', d*0.1))
nut.finish()
return Solid(
part=nut,
bottom=Axis(-0.5*h*Z, -Z, interval=(0,h)),
top=Axis(0.5*h*Z, Z, interval=(0,h)),
)
''' Iso hexagon nuts according to [EN ISO 4032](https://www.fasteners.eu/standards/ISO/4032/)
columns:
* thread
* w
* h
'''
standard_hexnuts = [
(1.6, 3.2, 1.3),
(2, 4, 1.6),
(2.5, 5, 2),
(3, 5.5, 2.4),
(3.5, 6, 2.8), # non-prefered
(4, 7, 3.2),
(5, 8, 4.7),
(6, 10, 5.2),
(8, 13, 6.8),
(10, 16, 8.4),
(12, 18, 10.8),
(14, 21, 12.8), # non-prefered
(16, 24, 14.8),
(18, 27, 15.8), # non-prefered
(20, 30, 18),
(22, 34, 19.4), # non-prefered
(24, 36, 21.5),
(27, 41, 23.8), # non-prefered
(30, 46, 25.6),
(33, 50, 38.7), # non-prefered
(36, 55, 31),
(39, 60, 33.4), # non-prefered
(42, 65, 34),
(45, 70, 36), # non-prefered
(48, 75, 38),
(52, 80, 42), # non-prefered
(56, 85, 45),
(60, 90, 48), # non-prefered
(64, 95, 51),
]
# -------------- washer stuff ----------------------
[docs]@cachefunc
def washer(d, e=None, h=None) -> Mesh:
''' Create a standard washer.
Washers are useful to offset screws and avoid them to scratch the mount part
Parameters:
d: the nominal interior diameter (screw or anything else),
the exact washer interior is slightly bigger
e: exterior diameter
h: height/thickness
If `d` alone is given, the other parameters default to the ISO specs: https://www.engineersedge.com/iso_flat_washer.htm
'''
if e is None and h is None:
args = standard_washers[bisect(standard_washers, d, key=itemgetter(0))]
if abs(args[0] - d)/d < 0.2:
_, d, e, h = args
else:
raise ValueError('no standard nut for the given diameter')
else:
d *= 1.1
if e is None: e = d*2
if h is None: h = d*0.1
surf = blendpair(
Circle((O,-Z), d/2),
Circle((O,Z), e/2),
tangents='straight',
)
return Solid(
part=extrusion(h*Z, surf),
top=Axis(h*Z, Z, interval=(0,h)),
bottom=Axis(O, -Z, interval=(0,h)),
)
''' Metric washers according to https://www.engineersedge.com/iso_flat_washer.htm
columns;
* nominal screw
* interior size
* exterior size
* thickness
'''
standard_washers = [
(1.6, 1.7, 4, 0.3),
(2, 2.2, 5, 0.3),
(2.5, 2.7, 6, 0.5),
(2.6, 2.8, 7, 0.5),
(3, 3.2, 7, 0.5),
(3.5, 3.7, 8, 0.5),
(4, 4.3, 9, 0.8),
(5, 5.3, 10, 1),
(6, 6.4, 12, 1.6),
(7, 7.4, 14, 1.6),
(8, 8.4, 16, 1.6),
(10, 10.5, 20, 2),
(12, 13, 24, 2.5),
(14, 15, 28, 2.5),
(16, 17, 30, 3),
(18, 19, 34, 3),
(20, 21, 37, 3),
(22, 23, 39, 3),
(24, 25, 44, 4),
(27, 28, 50, 4),
(30, 31, 56, 4),
(33, 34, 60, 5),
(36, 37, 66, 5),
(39, 40, 72, 6),
(42, 43, 78, 7),
(45, 46, 85, 7),
(48, 50, 92, 8),
(52, 54, 98, 8),
(56, 58, 105, 9),
]
[docs]def section_s(height=1, width=None, flange=None, thickness=None) -> Web:
''' Standard S (short flange) section. Very efficient to support flexion efforts.
'''
if width is None: width = 0.4 * height
if flange is None: flange = 0.036 * height
if thickness is None: thickness = 0.054 * height
assert width > 3*thickness+2*flange and height > 2*flange+2*thickness
base = wire([
vec3(width/2, height/2, 0),
vec3(width/2, height/2-flange, 0),
vec3(thickness/2, height/2-flange-0.1*(width/2-thickness/2), 0),
]) .flip()
base = base + base.transform(scaledir(X,-1)).flip()
base = base + base.transform(scaledir(Y,-1)).flip()
section = web(base.close().segmented())
bevel(section, section.frontiers(0,5,10,4,6,11), ('radius', thickness*0.4), resolution=('div',2))
bevel(section, section.frontiers(1,0,4,3,6,7,10,9), ('radius', flange), resolution=('div',2))
#notes = [
#note_distance(base.points[0], base.points[0]*vec3(1,-1,1), offset=2*flange*X),
#note_distance(base.points[0], base.points[1], offset=flange*X),
#note_distance(base.points[0], base.points[0]*vec3(-1,1,1), offset=flange*Y),
#note_distance(thickness/2*X, -thickness/2*X),
#]
return section.finish()
[docs]def section_w(height=1, width=None, flange=None, thickness=None) -> Web:
''' Standard W (wide flange) section. It is slightly different than a S section in that the flanges are straight and are usally wider.
'''
if width is None: width = 0.6 * height
if flange is None: flange = 0.036 * height
if thickness is None: thickness = 0.054 * height
assert width > 3*thickness+2*flange and height > 2*flange+2*thickness
base = wire([
vec3(width/2, height/2, 0),
vec3(width/2, height/2-flange, 0),
vec3(thickness/2, height/2-flange, 0),
]) .flip()
base = (base + base.transform(scaledir(X,-1)).flip() ).segmented()
base = base + base.transform(scaledir(Y,-1)).flip()
base.close()
section = web(base)
bevel(section, section.frontiers(0,5,10,4,6,11), ('radius', thickness*0.4), resolution=('div',2))
bevel(section, section.frontiers(1,0,4,3,6,7,10,9), ('radius', flange*0.5), resolution=('div',2))
return section.finish()
[docs]def section_l(a=1, b=None, thickness=None) -> Wire:
''' Standard L section '''
if b is None: b = a
if thickness is None: thickness = 0.05*max(a,b)
assert a > 3*thickness and b > 3*thickness
section = wire([
vec3(0, 0, 0),
vec3(0, a, 0),
vec3(thickness, a, 0),
vec3(thickness, thickness, 0),
vec3(b, thickness, 0),
vec3(b, 0, 0),
]) .close() .segmented() .flip()
bevel(section, [2,3,4], ('radius', thickness*0.8), resolution=('div',2))
return section.finish()
[docs]def section_c(height=1, width=None, thickness=None) -> Web:
''' Standard C section '''
if width is None: width = 0.6*height
if thickness is None: thickness = 0.05*height
assert width > 3*thickness
base = wire([
vec3(0, height/2, 0),
vec3(width, height/2, 0),
vec3(width, height/2-thickness, 0),
vec3(thickness, height/2-thickness, 0),
]) .flip()
base = base + base.transform(scaledir(Y,-1)).flip()
section = web(base.close().segmented())
bevel(section, section.frontiers(0,1,5,7,6), ('radius', 0.8*thickness), resolution=('div',2))
return section.finish()
[docs]def section_tslot(size=1, slot=None, thickness=None, depth=None) -> Web:
''' Standard T-Slot section. That section features slots on each side to put nuts at any position.
'''
if slot is None: slot = 0.3*size
if thickness is None: thickness = 0.08*size
if depth is None: depth = 0.2*size
b = depth-thickness
center = Circle((O,-Z), size/2-depth-2*thickness)
base = wire([
vec3(size/2, size/2, 0),
vec3(size/2, slot/2+thickness, 0),
vec3(size/2-thickness, slot/2, 0),
vec3(size/2-thickness, slot/2+b, 0),
vec3(size/2-thickness-depth+b, slot/2+b, 0),
vec3(size/2-thickness-depth, slot/2, 0),
]) .flip()
base = (base + base.transform(scaledir(normalize(vec3(1,-1,0)), -1)) .flip() ).segmented()
base = base + base.transform(scaledir(X, -1)) .flip()
base = base + base.transform(scaledir(Y, -1)) .flip()
section = web([
center,
base.close().segmented(),
])
bevel(section, section.frontiers(5,7, 41,43, 31,29, 17,19), ('radius', thickness/2), resolution=('div',2))
#notes = [
#note_distance(base.points[2], base.points[5], offset=-c/2*Y),
#note_distance(base.points[1], base.points[2], offset=-c/2*Y, project=X),
#note_distance(base.points[2], base.points[2]*vec3(1,-1,1), offset=s*X),
#note_distance(base.points[0], base.points[0]*vec3(1,-1,1), offset=2*s*X),
#]
return section.finish()
''' Standard IPN sections (S sections)
columns:
- h
- b
- s
- t
'''
standard_ipn = [
(80, 42, 3.9, 5.9),
(100, 50, 4.5, 6.8),
(120, 58, 5.1, 7.7),
(140, 66, 5.7, 8.6),
(160, 74, 6.3, 9.5),
(180, 82, 6.9, 10.4),
(200, 90, 7.5, 11.3),
(220, 98, 8.1, 12.2),
(240, 106, 8.7, 13.1),
(260, 113, 9.4, 14.1),
(280, 119, 10.1, 15.2),
(300, 125, 10.8, 16.2),
(320, 131, 11.5, 17.3),
(340, 137, 12.2, 18.3),
(360, 143, 13.0, 19.5),
(380, 149, 13.7, 20.5),
(400, 155, 14.4, 21.6),
(450, 170, 16.2, 24.3),
(500, 185, 18.0, 27.0),
(550, 200, 19.0, 30.0),
(600, 215, 21.6, 32.4),
]
# --------------------- coilspring stuff ------------------------
[docs]@cachefunc
def coilspring_compression(length, d=None, thickness=None, solid=True):
''' Return a Mesh model of a croilspring meant for use in compression
Parameters:
length: the distance between its two ends
d: the exterior diameter (the coilspring can fit in a cylinder of that diameter)
thickness: the wire diameter of the spring (useless if solid is disabled)
solid: disable it to get only the tube path of the coil, and have a `Wire` as return value
'''
if not d: d = length*0.2
if not thickness: thickness = d*0.1
r = d/2 - thickness # coil radius
e = r # coil step
div = settings.curve_resolution(d*pi, 2*pi)
step = 2*pi/(div+1)
t = 0
t0, z0 = t, -0.5*length
top = []
for t in linrange(t0, t0 + 4*pi, step):
top.append( vec3(r*cos(t), r*sin(t), z0 + (t-t0)/(2*pi) * thickness) )
t0, z0 = t, -0.5*length + 2*thickness
coil = []
for t in linrange(t0, t0 + 2*pi * (length-4*thickness) / e, step):
coil.append( vec3(r*cos(t), r*sin(t), z0 + (t-t0)/(2*pi) * e) )
t0, z0 = t, 0.5*length - 2*thickness
bot = []
for t in linrange(t0, t0 + 4*pi, step):
bot.append( vec3(r*cos(t), r*sin(t), z0 + (t-t0)/(2*pi) * thickness) )
path = Wire(top) + Wire(coil).qualify('spring') + Wire(bot)
if not solid:
return path
return Solid(
part=tube(
flatsurface(Circle(
(path[0],Y),
thickness/2,
resolution=('div',6)
)) .flip(),
path,
),
axis=Axis(O, Z, interval=(-length/2, length/2)),
top=0.5*length*Z,
bottom=-0.5*length*Z,
)
[docs]@cachefunc
def coilspring_tension(length, d=None, thickness=None, solid=True):
''' Return a Mesh model of a croilspring meant for use in tension
Parameters:
length: the distance between its two hooks
d: the exterior diameter (the coilspring can fit in a cylinder of that diameter)
thickness: the wire diameter of the spring (useless if solid is disabled)
solid: disable it to get only the tube path of the coil, and have a `Wire` as return value
'''
if not d: d = length*0.2
if not thickness: thickness = d*0.1
r = d/2 - thickness # coil radius
e = r # coil step
# separate the coilspring in 3 parts: the coil and the 2 hooks at both ides
spring_length = length - 2*r
ncoil = floor(spring_length / thickness) - 0.5
hold = 0.5*length - r
# create coil
div = settings.curve_resolution(d*pi, 2*pi)
step = 2*pi/(div+1)
z0 = -0.5 * ncoil * thickness
coil = Wire([ vec3(r*cos(t), r*sin(t), z0 + t/(2*pi) * thickness)
for t in linrange(0, 2*pi * ncoil, step) ]) .qualify('spring')
# create path with hooks
path = wire([
ArcCentered((-0.5*length*Z, X), vec3(0, -r, -0.5*length), -hold*Z),
ArcThrough(-hold*Z, (-hold*Z + coil[0])*0.5 - 0.5*r*Y, coil[0]),
coil,
ArcThrough(coil[-1], (hold*Z + coil[-1])*0.5 - 0.5*r*Y, hold*Z),
ArcCentered((0.5*length*Z, X), hold*Z, vec3(0, -r, 0.5*length)),
])
if not solid:
return path
return Solid(
part=tube(
flatsurface(Circle(
(path[0],Z),
thickness/2,
resolution=('div',6)
)),
path,
),
axis=Axis(O,Z, interval=(-length/2, length/2)),
top=0.5*length*Z,
bottom=-0.5*length*Z,
)
[docs]@cachefunc
def coilspring_torsion(arm,
angle=radians(45),
d=None,
length=None,
thickness=None,
hook=None,
solid=True) -> Solid:
''' Return a Mesh model of a croilspring meant for use in torsion
Parameters:
arm: the arms length from the coil axis
length: the coil length (and distance between its hooks)
d: the exterior diameter (the coilspring can fit in a cylinder of that diameter)
thickness: the wire diameter of the spring (useless if solid is disabled)
hook: the length of arm hooks (negative for hooks toward the interior)
solid: disable it to get only the tube path of the coil, and have a `Wire` as return value
'''
if not length: length = arm*0.5
if not d: d = arm
if not thickness: thickness = d*0.1
if not hook: hook = -length
r = d/2 - thickness # coil radius
e = r # coil step
angle = pi - angle
# separate the coilspring in 3 parts: the coil and the 2 hooks at both ides
ncoil = ceil(length / thickness) + angle/(2*pi)
# create coil
div = settings.curve_resolution(d*pi, 2*pi)
step = 2*pi/(div+1)
z0 = -0.5 * ncoil * thickness
coil = Wire([ vec3(-r*sin(t), r*cos(t), z0 + t/(2*pi) * thickness)
for t in linrange(0, 2*pi * ncoil, step) ]) .qualify('spring')
# create hooks
c = thickness * sign(hook)
if abs(c) > abs(hook):
hook = c
top = Wire([
vec3(arm, 0, hook),
vec3(arm, 0, c),
vec3(arm-abs(c), 0, 0),
vec3(0),
]) .transform(coil[0])
bot = (top
.flip()
.transform(scaledir(Z,-1)*scaledir(X,-1))
.transform(angleAxis(angle, Z))
)
# create path
path = top + coil + bot
path.mergeclose()
if not solid:
return path
return Solid(
part=tube(
flatsurface(Circle(
(path[0], normalize(path[0]-path[1])),
thickness/2,
resolution=('div',6),
)),
path,
),
axis=Axis(O,Z, interval=(-length/2, length/2)),
top=path[0],
bottom=path[-1],
)
# ----------------------- bearing stuff --------------------------
[docs]@cachefunc
def bearing(dint, dext=None, h=None,
circulating='ball',
contact=0,
hint=None, hext=None,
sealing=False,
detail=False) -> Solid:
'''
Circulating bearings rely on rolling elements to avoid friction and widen the part life.
Its friction depends on the rotation speed but not on the current load.
See bearing specs at https://koyo.jtekt.co.jp/en/support/bearing-knowledge/
Parameters:
dint: interior bore diameter
dext: exterior ring diameter
h: total height of the bearing
hint: height of the interior ring. Only for angled roller bearings
hext: height of the exterior ring. Only for angled roller bearings
circulating:
The type of circulating element in the bearing
- ball
- roller
contact:
Contact angle (aka pressure angle).
It decided what directions of force the bearing can sustain.
- `0` for radial bearing
- `pi/2` for thrust (axial) bearings
- `0 < contact < pi/2` for conic bearings
sealing: True if the bearing has a sealing side. Only for balls bearings with `contact = 0`
detail: If True, the returned model will have the circulating elements and cage, if False the returned element is just a bounding representation
'''
if isinstance(dint, str):
dint, dext, h = bearing_spec(dint)
else:
if not dext: dext = 2*dint
if not h: h = 0.5*dint
assert 0 < h <= dint < dext
assert 0 <= contact
if circulating == 'roller':
if sealing:
raise ValueError('sealing must be None for roller bearings')
return bearing_roller(dint, dext, h, contact, hint, hext, detail)
elif circulating == 'ball':
if hint or hext:
raise ValueError('hint and hext must be None for ball bearings')
if abs(contact) < NUMPREC:
return bearing_ball(dint, dext, h, sealing, detail)
if abs(contact - 0.5*pi) < NUMPREC:
if sealing:
raise ValueError('sealing must be none for thrust bearings')
return bearing_thrust(dint, dext, h, detail)
else:
raise ValueError('ball bearings must not be angled')
else:
raise ValueError('unknown circulating element {}'.format(repr(circulating)))
def bearing_spec(code):
# iso code
if ' ' in code:
indev
# simple code
elif re.match(r'[\d\.]+x[\d\.]+x[\d\.]+', code):
return tuple(float(d) for d in re.split('x'))
bearing_color = vec3(0.5,0.4,0.35)
bearing_cage_color = vec3(0.3,0.2,0)
bearing_circulating_color = vec3(0,0.1,0.2)
def bearing_ball(dint, dext=None, h=None, sealing=False, detail=False) -> Solid:
# convenient variables
rint = dint/2
rext = dext/2
c = 0.05*h
w = 0.5*h
e = 0.15*(dext-dint)
# outer rings profiles
axis = Axis(O,Z, interval=(0,h))
interior = Wire([
vec3(rint+e, 0, w),
vec3(rint, 0, w),
vec3(rint, 0, -w),
vec3(rint+e, 0, -w),
]) .segmented() .flip()
bevel(interior, [1, 2], ('radius',c), resolution=('div',1))
exterior = Wire([
vec3(rext-e, 0, -w),
vec3(rext, 0, -w),
vec3(rext, 0, w),
vec3(rext-e, 0, w),
]) .segmented() .flip()
bevel(exterior, [1,2], ('radius',c), resolution=('div',1))
if detail and not sealing:
rb = (dint + dext)/4 # balls path radius
rr = 0.75*(dext - dint)/4 # balls radius
hr = sqrt(rr**2 - (rb-rint-e)**2) # half balls inprint width in the rings
interior += wire(ArcCentered((rb*X,-Y), vec3(rint+e, 0, hr), vec3(rint+e, 0, -hr)))
exterior += wire(ArcCentered((rb*X,-Y), vec3(rext-e, 0, -hr), vec3(rext-e, 0, hr)))
interior.close()
exterior.close()
part = revolution(2*pi, axis, web([exterior, interior])) .option(color=bearing_color)
nb = int(0.7 * pi*rb/rr) # number of balls that can fit in
balls = repeat(icosphere(rb*X, rr), nb, angleAxis(radians(360)/nb, Z)) .option(color=vec3(0,0.1,0.2))
# balls cage (simplified version)
cage_profile = Wire([vec3(
rb*cos(t),
rb*sin(t),
(rr+0.5*c) * (1 - (0.5-0.5*cos(nb*t))**2) + c)
for t in linrange(0, 2*pi, div=nb*20-1)])
cage_profile.mergeclose()
surf = extrusion(
mat3(
(rb-rr*0.6)/rb,
(rb-rr*0.6)/rb,
1),
cage_profile,
alignment=0.5)
cage = thicken(
surf + surf.transform(mat3(1,1,-1)) .flip(),
c) .option(color=bearing_cage_color)
# asemble
return Solid(part=part, cage=cage, balls=balls, axis=axis)
else:
# assemble and close rings profiles
interior = (
Wire([
exterior[-1],
exterior[-1]+c*Z,
interior[0]+c*Z,
interior[0]]) .segmented()
+ interior
+ Wire([
interior[-1],
interior[-1]-c*Z,
exterior[0]-c*Z,
exterior[0]]) .segmented()
)
return Solid(
part=revolution(2*pi, axis, web([exterior, interior]))
.option(color=bearing_color),
axis=axis)
def bearing_roller(dint, dext=None, h=None, contact=0, hint=None, hext=None, detail=False) -> Solid:
# interior and exterior heights (automatically deduced from total height if not specified)
if not hint: hint = h*cos(contact)
if not hext: hext = h*cos(contact) * 0.8 if contact else h
assert 0 < hint <= h
assert 0 < hext <= h
# convenient variables
rint = dint/2
rext = dext/2
c = 0.05*h
w = 0.5*h
e = 0.08*(dext-dint)
axis = Axis(O,Z, interval=(0,h))
# cones definition points
if contact:
# axis on a cone using the given contact angle
cr = vec3(mix(rint, rext, 0.5), 0, 0)
ct = -cr.x / tan(contact) *Z
angled = Axis(ct, vec3(sin(contact), 0, cos(contact)))
p1 = vec3(rext-e, 0, -w+hext)
p2 = vec3(rint+e, 0, w-hint+e)
a1 = Axis(p1, normalize(p1-ct))
a2 = Axis(p2, normalize(p2-ct))
p3 = angled[0] - project(reflect(p2-angled[0], angled[1]), a1[1])
p4 = angled[0] - reflect(p3-angled[0], angled[1])
p5 = angled[0] - reflect(p1-angled[0], angled[1])
else:
# cylindrig bearing: infinite cone
p1 = vec3(rext-e, 0, -w+hext-e)
p3 = vec3(rext-e, 0, -w+e)
p4 = vec3(rint+e, 0, w-hint+e)
p5 = vec3(rint+e, 0, -w+hext-e)
angled = Axis(0.5*(rint+rext) * X, Z)
# ring profiles
interior = Wire([
p5+e*X,
vec3(p5[0]+e, 0, w),
vec3(rint, 0, w),
vec3(rint, 0, w-hint),
vec3(p4[0]+e, 0, w-hint),
p4+e*X,
]) .segmented()
exterior = Wire([
p3,
vec3(p3[0], 0, -w),
vec3(rext, 0, -w),
vec3(rext, 0, -w+hext),
vec3(rext-e, 0, -w+hext),
]) .segmented()
bevel(interior, [2,3], ('radius',c), resolution=('div',1))
bevel(exterior, [2,3], ('radius',c), resolution=('div',1))
# create interior details
if detail:
# complete rings with their interiors
interior += Wire([
p4,
p5,
p5+e*X,
]) .segmented()
exterior += Wire([p3])
part = revolution(2*pi, axis, web([
exterior,
interior,
]).flip() ) .option(color=bearing_color)
# create conic rollers
roller = revolution(2*pi, angled, Segment(mix(p1,p3,0.05), mix(p3,p1,0.05)))
for hole in roller.outlines().islands():
roller += flatsurface(wire(hole))
# number of rollers that can fit in
nb = int(pi*(rint+rext) / (2.5*distance_pa(p1,angled)))
rollers = repeat(roller, nb, rotatearound(2*pi/nb, axis))
rollers.option(color=bearing_circulating_color)
# roller cage
p6 = mix(p4,p3,0.6) - e*angled[1]
cage_profile = wire([
p6 - 1.5*e*X,
p6,
p6 + (distance(p1,p3)+1.8*e) * angled[1],
])
bevel(cage_profile, [1], ('radius',c))
cage = revolution(2*pi, axis, cage_profile)
cage = pierce(cage, inflate(rollers, 0.5*c), False)
cage = thicken(cage, c) .option(color=bearing_cage_color)
# assemble
return Solid(part=part, cage=cage, rollers=rollers, axis=axis)
# simply create a bounding representation
else:
# assemble and close rings profiles
return Solid(
part=revolution(2*pi, axis,
(exterior + interior) .close() .flip()
) .option(color=bearing_color),
axis=axis)
def bearing_thrust(dint, dext, h, detail=False) -> Solid:
# convenient variables
rint = dint/2
rext = dext/2
c = 0.05*h # corner radius
w = 0.5*h # half height
e = 0.12*(dext-dint) # outer rings thickness
# rings outlines
axis = Axis(O,Z, interval=(0,h))
top = Wire([
vec3(rext, 0, w-e),
vec3(rext, 0, w),
vec3(rint, 0, w),
vec3(rint, 0, w-e),
]) .segmented() .flip()
bevel(top, [1, 2], ('radius',c), resolution=('div',1))
bot = Wire([
vec3(rint, 0, -w+e),
vec3(rint, 0, -w),
vec3(rext, 0, -w),
vec3(rext, 0, -w+e),
]) .segmented() .flip()
bevel(bot, [1,2], ('radius',c), resolution=('div',1))
if detail:
rb = (dint + dext)/4 # balls guide radius
rr = 0.75*h/2 # ball radius
hr = sqrt(rr**2 - (w-e)**2) # half ball inprint width in the rings
# complete the rings with their interior
top += wire(ArcCentered((rb*X,-Y), vec3(rb+hr, 0, w-e), vec3(rb-hr, 0, w-e)))
bot += wire(ArcCentered((rb*X,-Y), vec3(rb-hr, 0, -w+e), vec3(rb+hr, 0, -w+e)))
top.close()
bot.close()
part = revolution(2*pi, axis, web([top, bot])) .option(color=bearing_color)
# number of balls to place
nb = int(0.8 * pi*rb/rr)
balls = repeat(icosphere(rb*X, rr), nb, angleAxis(radians(360)/nb, Z))
balls.option(color=bearing_circulating_color)
# cage
cage_profile = Wire([
vec3(rext-c, 0, -w+e+0.1*h),
vec3(rext-c, 0, w-e-0.1*h),
vec3(rint+c, 0, w-e-0.1*h),
vec3(rint+c, 0, -w+e+0.1*h),
])
bevel(cage_profile, [1,2], ('radius',c), resolution=('div',1))
cage_surf = revolution(2*pi, axis, cage_profile)
cage_surf = pierce(cage_surf, inflate(balls, 0.2*c), False)
cage = thicken(cage_surf, c) .option(color=bearing_cage_color)
# assemble
return Solid(part=part, cage=cage, balls=balls, axis=axis)
else:
# assemble and close the profiles
top = (
Wire([
bot[-1],
bot[-1]+c*X,
top[0]+c*X,
top[0]]) .segmented()
+ top
+ Wire([
top[-1],
top[-1]-c*X,
bot[0]-c*X,
bot[0]]) .segmented()
)
return Solid(
part=revolution(2*pi, axis, web([top, bot])) .option(color=bearing_color),
axis=axis)
'''
all existing bearing dimensions according to https://koyo.jtekt.co.jp/en/support/bearing-knowledge/6-3000.html
'''
standard_bearing_ball_straight = [
(16, 35, 11),
]
from .selection import *
[docs]@cachefunc
def slidebearing(dint, h=None, thickness=None, shoulder=None, opened=False) -> Solid:
'''
Slide bearings rely on gliding parts to ensure a good pivot. It's much cheaper than circulating bearings and much more compact. But needs lubricant and has a shorter life than circulating bearings.
Its friction depends on the rotation speed and on the load.
Parameters:
dint: interior diameter
h: exterior height (under shoulder if there is)
thickness: shell thickness, can be automatically determined
shoulder: distance from bore to shoulder tip, put 0 to disable
opened: enable to have a slight breach that allows a better fitting to the placement hole
'''
if h is None: h = 1.2*dint
if thickness is None: thickness = 0.05*dint
rint = dint/2
profile = Wire([
vec3(rint,0,-h),
vec3(rint,0, 0),
])
if shoulder:
profile += Wire([ vec3(rint+shoulder, 0, 0) ])
profile = profile.segmented()
bevel(profile, [1], ('radius', 1.5*thickness), resolution=('div',2))
axis = Axis(O,Z, interval=(-h,0))
shape = revolution(1.98*pi if opened else 2*pi, axis, profile)
part = thicken(shape, thickness) .option(color=bearing_color)
line = ( select(part, vec3(-rint,0,-h), stopangle(pi/2))
+ select(part, vec3(dint,dint,-h), stopangle(pi/2))
)
if not shoulder:
line += ( select(part, vec3(-rint,0,0), stopangle(pi/2))
+ select(part, vec3(dint,dint,0), stopangle(pi/2))
)
chamfer(part, line, ('width',0.5*thickness))
return Solid(part=part, axis=axis)
[docs]def bolt(a: vec3, b: vec3, dscrew: float, washera=False, washerb=False) -> Solid:
''' convenient function to create a screw, nut and washers assembly
Parameters:
a: the screw placement
b: the nut placement
dscrew: the screw `d` parameter
washera: if True, place a washer between the screw head and `a`
washerb: if True, place a washer between the nut and `b`
'''
dir = normalize(b-a)
rwasher = washer(dscrew)
thickness = rwasher['part'].box().width.z
rscrew = screw(dscrew, stceil(distance(a,b) + 1.2*dscrew, precision=0.2))
rnut = nut(dscrew)
#rscrew['annotations'] = [
#note_distance(O, -stceil(distance(a,b) + 1.2*dscrew)*Z, offset=2*dscrew*X),
#note_radius(rscrew['part'].group(0)),
#]
from .joints import Pivot
return Solid(
screw = rscrew.place((Pivot, rscrew['axis'], Axis(a-thickness*dir, -dir))),
nut = rnut.place((Pivot, rnut['top'], Axis(b+thickness*dir, -dir))),
w1 = rwasher.place((Pivot, rwasher['top'], Axis(b, -dir))),
w2 = rwasher.place((Pivot, rwasher['top'], Axis(a, dir))),
)
[docs]def screw_slot(axis: Axis, dscrew: float, rslot=None, hole=True, expand=True) -> Mesh:
''' slot shape for a screw
the result can then be used in a boolean operation to reserve set a screw place in an arbitrary shape
Parameters:
axis: the screw axis placement, z toward the screw head (part exterior)
dscrew: the screw diameter
rslot: the screw head slot radius
hole:
- if `True`, enables a cylindric hole for screw body
- if `float`, it is the screw hole length
expand:
- if `True`, enables slots sides
- if `float`, it is the slot sides height
'''
if not rslot: rslot = 1.1*dscrew
o = axis[0]
x,y,z = dirbase(axis[1])
profile = []
if expand:
if isinstance(expand, bool): expand = 2*rslot
profile.append(o + rslot*x + expand*z)
profile.append(o + rslot*x)
if hole:
if isinstance(hole, bool): hole = dscrew*3
profile.append(o + 0.5*dscrew*x)
profile.append(o + 0.5*dscrew*x - hole*z)
profile.append(o - (hole+0.5*dscrew)*z)
else:
profile.append(o)
return revolution(2*pi, Axis(o,-z), wire(profile).segmented()).finish()
[docs]def bolt_slot(a: vec3, b: vec3, dscrew: float, rslot=None, hole=True, expanda=True, expandb=True) -> Mesh:
''' bolt shape for a screw
musch like `screw_slot()` but with two endings
Parameters:
a: position of screw head
b: position of nut
dscrew: the screw diameter
rslot: the screw head slot radius
hole: enabled the cylindric hole between the head and nut slots
expanda, expandb:
- if `True`, enables slot sides on tip `a` or `b`
- if `float`, it is the slot side height
Example:
>>> a, b = vec3(...), vec3(...)
>>> bolts = bolt(a, b, 3)
>>> part_hole = bolt_slot(a, b, 3)
'''
if not rslot: rslot = 1.3*dscrew
x,y,z = dirbase(normalize(a-b))
def one_slot(o,x,z, expand):
profile = []
if expand:
if isinstance(expand, bool): expand = 2*rslot
profile.append(o + rslot*x + expand*z)
profile.append(o + rslot*x)
if hole:
profile.append(o + 0.5*dscrew*x)
else:
profile.append(o)
return wire(profile).segmented()
return revolution(2*pi, Axis(a,-z),
one_slot(a,x,z, expanda) + one_slot(b,x,-z, expandb).flip()
).finish()
[docs]def bearing_slot_exterior(axis: Axis, dext: float, height: float, shouldering=True, circlip=False, evade=True, expand=True):
''' slot for a bearing exterior ring, such as returned by `bearing()`
Parameters:
axis: bearing axis placement
dext: bearing `dext` parameter
height: bearing `h` parameter
shouldering: generate a shoulder to support the top side of the bearing's exterior ring
circlip: enable a slot for a circlip to support the bottom side of the bearing's exterior ring
evade: set expansion to evasive rather that straight
expand:
expand the slot with evasive or straight surfaces to ease itnersection with other meshes
- `True` enables expansion with a default length
- `float` set the expansion distance
'''
rext = dext/2
tol = stceil(rext*1e-2)
profile = [
rext*X + 0.5*height*Z,
rext*X - 0.5*height*Z,
]
if shouldering:
profile.insert(0, profile[0] - 0.17*rext*X)
if circlip:
circlip_height = stceil(0.1*rext)
circlip_depth = stceil(1.05*rext) - rext
profile[-1:] = accumulate([
rext*X - (0.5*height + tol)*Z,
circlip_depth*X,
-circlip_height*Z,
-circlip_depth*X,
-0.5*circlip_height*Z,
])
if expand == True: expand = 0.4*rext
if evade:
if shouldering:
profile.insert(0, profile[0] + 0.1*rext*Z)
profile.append(profile[-1] + expand*(X-Z))
profile.insert(0, profile[0] + expand*(X+Z))
elif expand:
profile.extend([
profile[-1] - 0.05*rext*(Z-X),
profile[-1] - 0.05*rext*(Z-X) - expand*Z,
])
profile.insert(0, profile[0] + expand*Z)
return revolution(2*pi, Axis(O,-Z), wire(profile).segmented()) .transform(translate(axis[0]) * mat4(quat(Z, axis[1])))
[docs]def bearing_slot_interior(axis: Axis, dint: float, height: float, shouldering=True, circlip=False, evade=True, expand=True) -> Mesh:
''' slot for a bearing interior ring, such as returned by `bearing()`
Parameters:
axis: bearing axis placement
dint: bearing `dint` parameter
height: bearing `h` parameter
shouldering: generate a shoulder to support the top side of the bearing's interior ring
circlip: generate a slot for a circlip to support the bottom side of the bearing's interior ring
evade: set expansion to evasive rather that straight
expand:
expand the slot with evasive or straight surfaces to ease itnersection with other meshes
- `True` enables expansion with a default length
- `float` set the expansion distance
'''
rint = dint/2
tol = stceil(rint*2e-2)
profile = [
rint*X + 0.5*height*Z,
rint*X - 0.5*height*Z,
]
if shouldering:
profile.insert(0, profile[0] + 0.36*rint*X)
if circlip:
circlip_height = stceil(0.2*rint)
circlip_depth = rint - stceil(0.8*rint)
profile[-1:] = accumulate([
rint*X - (0.5*height + tol)*Z,
-circlip_depth*X,
-circlip_height*Z,
circlip_depth*X,
-0.5*circlip_height*Z,
])
if expand == True: expand = 0.8*rint
if evade:
if shouldering:
profile.insert(0, profile[0] + 0.1*rint*Z)
profile.append(profile[-1] - expand*(X+Z))
profile.insert(0, profile[0] - expand*(X-Z))
elif expand:
profile.extend([
profile[-1] - 0.1*rint*(Z+X),
profile[-1] - 0.1*rint*(Z+X) - expand*Z,
])
profile.insert(0, profile[0] + expand*Z)
return revolution(2*pi, Axis(O,Z), wire(profile).segmented()) .transform(translate(axis[0]) * mat4(quat(Z, axis[1])))
'''
* profilés
+ ipn
+ (carrés a rails)
+ U
+ L
* nut
+ carrés
+ hex
+ auto-stopable
+ fermés au bout
+ support
* screw
+ forme tete (UH, BH, VH, ...)
+ forme empreinte (torx, hex, crux, line, ...)
* rondelle
+ simple
+ stobable
+ ressort
+ entretoise
* roulement
+ bille, rouleau
+ droit, oblique, radial
+ etancheité
* coussinet
+ droit
+ a epaule
+ fendu
* anneaux elastiques
+ circlip interieur/exterieur
+ ressort
* helice
+ rotor de ventilateur ...
* vis/ecrou
+ a billes
+ a profil trapezoidal
* accouplements
+ ressort
+ encastrement: prisme, cannelures, clavete, meplat
* ressorts
+ lineaire
+ extension
+ conique
+ pli
+ spiral
+ concentrique
+ lame
* tuyau
+ embouchure
+ droit/coude
+ le long d'un chemin ?
pour tout:
- liste des tailles standard
'''