Compare commits

...

19 Commits

Author SHA1 Message Date
dc45f27141 Add documentation and example code 2022-03-06 05:48:15 +01:00
c07dee1dd5 Center flex 2022-02-28 00:35:17 +01:00
5888485b16 5AA design works all around 2022-02-28 00:29:14 +01:00
cbc847ea42 Automatically adjust padding; make plug hexagonal 2022-02-27 02:40:53 +01:00
9fa39a7a34 Implement lid locking mechanism 2022-02-26 05:59:30 +01:00
4c8ca5db03 Flex holds up 2022-02-19 21:16:46 +01:00
33e8a83404 Flex (barely) holds up 2021-11-26 04:51:34 +01:00
2f9c97913e Finer flex, flexible but too brittle 2021-11-26 00:13:05 +01:00
e169dfb9f5 Revised flex case; not yet flexible enough 2021-11-25 04:28:19 +01:00
eeb07bfb44 Tweak placement 2021-11-24 05:14:02 +01:00
bcf34f8976 Draw rim and lid 2021-11-24 04:51:25 +01:00
144f6d951e Draw rounded hexagonal base plate 2021-11-24 04:07:13 +01:00
d2a9a0b8e0 Rounded hexagons for interior planes 2021-11-24 04:06:01 +01:00
bf1433afe7 Flex case experiment: Not structurally sound 2021-11-21 01:01:14 +01:00
5dd4fe4117 First prototype of bottom case: sides don't overlap enough, interior guides too widely apart, should align with fingers 2021-11-15 03:54:48 +01:00
1159f28855 Draw bottom / top 2021-11-15 01:12:58 +01:00
16e60427d1 Pincushion experiment 2021-11-14 06:07:31 +01:00
c19a1e8876 Start battery case generator 2021-11-14 06:07:15 +01:00
d0e64ae692 Add simple turtle graphics generator 2021-11-14 06:06:53 +01:00
4 changed files with 603 additions and 0 deletions

336
battery-case-generator.py Executable file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
import math
import sys
import argparse
import svgturtle
parser = argparse.ArgumentParser(description='Generate a hexagonal battery case')
parser.add_argument('--dimension', default=5, type=int, help='Grid dimension')
parser.add_argument('--height', type=float, help='Height of battery')
parser.add_argument('--hole', default='AA', help='Hole diameter (mm or A, AA, AAA)')
parser.add_argument('--kerf', default=.1, type=float, help='Kerf')
parser.add_argument('--corner-length', default=4, help='Length of stretch corner in material thicknesses')
parser.add_argument('--stretch', default=1.05, type=float, help='Reduction factor of stretch material')
parser.add_argument('--horizontal-finger', default=5.0, type=float, help='Width of horizontal fingers')
parser.add_argument('--vertical-finger', default=15.0, type=float, help='Width of vertical fingers')
parser.add_argument('--padding', default=1.5, type=float, help='Padding around holes')
parser.add_argument('--outside-padding', default=4.0, type=float, help='Extra padding between holes and wall')
parser.add_argument('--extra-height', default=2.0, type=float, help='Extra vertical space')
parser.add_argument('--thickness', default=3.0, type=float, help='Thickness of material')
parser.add_argument('--lid', default=0.2, type=float, help='How much extra play to give the lid')
parser.add_argument('--tooth', default=0.8, type=float, help='How much to round the edges of the teeth')
parser.add_argument('--flex-width', default=.5, type=float, help='Spacing (in material thickness) between flex lines')
parser.add_argument('--flex-cut', default=5.0, type=float, help='Length (in material thickness) of flex cuts')
parser.add_argument('--flex-gap', default=1.0, type=float, help='Gap (in material thickness) between flex cuts')
parser.add_argument('--plug-play', default=0.8, type=float, help='How much smaller to make the plug than the hole')
parser.add_argument('--verbose', action='store_true', help='Print computed parameter values')
args = parser.parse_args()
assert (args.dimension % 2) == 1
BATTERY = {
'AAA': [10.5, 44.5],
'AA': [14.5, 50.5],
'C': [26.2, 50.0],
'D': [34.2, 61.5],
}
if args.hole in BATTERY:
dim = BATTERY[args.hole]
args.hole = dim[0]
if args.height == None:
args.height = dim[1]
else:
args.hole = float(args.hole)
assert(args.height != None)
SUITABLE = False
while not SUITABLE:
args.kerf2 = args.kerf/2
args.grid = args.hole+args.padding
args.radius = args.hole/2
args.corner = args.corner_length*args.thickness
args.corner_s = args.corner*args.stretch
args.corner_radius = 3*args.corner_s/math.pi
args.plug_radius = args.corner_radius-args.thickness
args.corner_inset = args.corner_s*math.sqrt(3)/math.pi
args.plug_inset = args.plug_radius/math.sqrt(3)
args.interior_edge = args.grid*args.dimension*0.5+args.outside_padding
args.opening_edge = args.interior_edge-args.thickness
args.plug_edge = min(args.opening_edge, .5*math.sqrt(3)*args.interior_edge)-args.plug_play
args.disc_radius = 0.45*(math.sqrt(3)-1)*args.plug_edge+args.plug_inset
args.exterior_edge = args.interior_edge+2.0*args.thickness
args.interior_leg = (args.interior_edge-args.horizontal_finger-args.kerf)/2-args.corner_inset
finger_length = args.interior_edge-2.0*args.corner_inset
args.n_hor_fingers = max(int(finger_length/args.horizontal_finger/2), 1)
args.exterior_leg = (finger_length-(2*args.n_hor_fingers-1)*args.horizontal_finger+args.kerf)/2
args.wall_leg = (args.interior_edge-args.corner-(2*args.n_hor_fingers-1)*args.horizontal_finger+args.kerf)/2
args.exterior_slot = (args.interior_edge-args.horizontal_finger+args.kerf)/2
args.n_ver_fingers = int((args.height+args.extra_height)/args.vertical_finger)
top_slot = args.extra_height+args.thickness+0.45*args.height
args.slots = [top_slot, top_slot+15.0]
if args.exterior_leg > 2:
SUITABLE=True
else:
args.outside_padding += .5
args.padding += .2 # Try again with more padding
if args.verbose:
print(args, file=sys.stderr)
BOX = 2.0*args.exterior_edge+5.0
DIMX = 3.0*BOX
DIMY = 2.0*BOX+args.height+args.extra_height+3.0*args.thickness+5.0
PI3 = math.pi/3
HOLES = ''
SHAPES = ''
MARKS = ''
def draw_grid(cx, cy):
global HOLES
for row in range(-int(args.dimension/2), int((args.dimension+1)/2)):
cyr = cy+args.grid*row*math.sin(PI3)
num_col = args.dimension-abs(row)
cxr = cx-.5*args.grid*(num_col-1.0)
for col in range(num_col):
HOLES += '<circle cx="%.2f" cy="%.2f" r="%.2f"/>\n' % (cxr+col*args.grid, cyr, args.radius)
def draw_disc(cx, cy, layer):
global HOLES, SHAPES, MARKS
turtle = svgturtle.SvgTurtle(cx, cy)
turtle.penup()
turtle.forward(args.disc_radius)
turtle.pendown()
turtle.left(90)
turtle.circle(args.disc_radius)
turtle.penup()
turtle.home()
if layer=='disc':
HOLES += '<path d="%s"/>\n' % turtle.to_s()
else:
MARKS += '<path d="%s"/>\n' % turtle.to_s()
def draw_plane(cx, cy, layer):
global HOLES, SHAPES, MARKS
if layer=='interior':
edge = args.interior_edge
elif layer=='opening':
edge = args.opening_edge
elif layer=='plug' or layer=='plug_mark':
edge = args.plug_edge
else:
edge = args.exterior_edge
turtle = svgturtle.SvgTurtle(cx, cy)
turtle.penup()
if layer=='plug_mark':
turtle.right(30)
turtle.forward(edge)
turtle.right(120)
if layer=='interior':
turtle.forward(args.corner_inset)
turtle.pendown()
for side in range(5):
turtle.forward(args.interior_leg)
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.horizontal_finger+args.kerf)
turtle.right(90)
turtle.forward(args.thickness)
turtle.left(90)
turtle.forward(args.interior_leg)
turtle.circle(-args.corner_radius, 60)
turtle.forward(edge-2.0*args.corner_inset)
turtle.circle(-args.corner_radius, 60)
elif layer=='plug' or layer=='plug_mark':
turtle.pendown()
for side in range(3):
turtle.forward(0.5*edge)
turtle.right(90)
turtle.circle(0.5*edge, 120)
turtle.right(90)
turtle.forward(0.5*edge)
turtle.right(60)
else:
turtle.pendown()
for side in range(6):
turtle.forward(edge)
turtle.right(60)
if layer=='plug':
HOLES += '<path d="%s"/>\n' % turtle.to_s()
elif layer=='plug_mark':
MARKS += '<path d="%s"/>\n' % turtle.to_s()
else:
SHAPES += '<path d="%s"/>\n' % turtle.to_s()
turtle.reset()
turtle.penup()
if layer=='bottom' or layer=='rim':
turtle.forward(args.interior_edge)
turtle.right(120)
turtle.forward(args.corner_inset)
for side in range(5):
for finger in range(args.n_hor_fingers):
turtle.forward(args.exterior_leg if finger == 0 else args.horizontal_finger+args.kerf)
turtle.pendown()
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.horizontal_finger-args.kerf)
turtle.right(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.horizontal_finger-args.kerf)
turtle.penup()
turtle.right(180)
turtle.forward(args.horizontal_finger-args.kerf)
turtle.forward(args.exterior_leg)
turtle.pendown()
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.circle(-args.corner_radius-args.thickness, 60)
turtle.right(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.circle(args.corner_radius, 60)
turtle.penup()
turtle.right(180)
turtle.circle(-args.corner_radius, 60)
turtle.forward(args.interior_edge-2*args.corner_inset)
turtle.pendown()
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.circle(-args.corner_radius-args.thickness, 60)
turtle.right(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.circle(args.corner_radius, 60)
HOLES += '<path d="%s"/>\n' % turtle.to_s()
def draw_flex(t, h):
global HOLES
turtle = svgturtle.SvgTurtle(t.x, t.y)
gap = args.flex_gap*args.thickness
ncut = max(int((h-gap) // (args.flex_cut*args.thickness)), 1)
cut = ((h-gap) / ncut) - gap
dx = args.flex_width*args.thickness
nlines = int(args.corner // dx)
x0 = .5*(args.corner - (nlines-1)*dx)
turtle.forward(x0)
for line in range(nlines):
turtle.pendown()
if (line % 2) == 0:
turtle.right(90)
turtle.forward(gap+cut)
for section in range(ncut-2):
turtle.penup()
turtle.forward(gap)
turtle.pendown()
turtle.forward(gap+2*cut)
turtle.penup()
turtle.forward(gap)
if (ncut % 2) == 0:
turtle.pendown()
turtle.forward(gap+cut)
turtle.penup()
turtle.left(90)
else:
turtle.left(90)
if (ncut % 2) == 1:
turtle.forward(gap+cut)
for section in range(ncut-1-(ncut % 2)):
turtle.penup()
turtle.forward(gap)
turtle.pendown()
turtle.forward(gap+2*cut)
turtle.penup()
turtle.forward(gap)
turtle.right(90)
turtle.forward(dx)
HOLES += '<path d="%s"/>\n' % turtle.to_s()
def draw_case_h(turtle, h, top):
turtle.forward(0.5*args.interior_edge-0.5*args.corner)
for side in range(6):
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
if top:
draw_flex(turtle, h+2*args.thickness)
turtle.forward(args.corner)
turtle.right(90)
turtle.forward(args.thickness)
turtle.left(90)
if side == 5:
break
turtle.forward(args.wall_leg-0.5*args.kerf)
for finger in range(args.n_hor_fingers):
if finger > 0:
turtle.forward(args.horizontal_finger-args.kerf)
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.horizontal_finger+args.kerf)
turtle.right(90)
turtle.forward(args.thickness)
turtle.left(90)
turtle.forward(args.wall_leg-0.5*args.kerf)
turtle.forward(0.5*args.interior_edge-0.5*args.corner)
def draw_case_v(turtle, h):
ey = turtle.y+h
leg = (h-(args.n_ver_fingers-0.66)*args.vertical_finger)/2.0
slope = args.vertical_finger/math.sqrt(18)
turtle.forward(leg)
for finger in range(args.n_ver_fingers):
ty = ey if finger==args.n_ver_fingers-1 else turtle.y+args.vertical_finger
turtle.circle(args.tooth, 135)
turtle.forward(slope)
turtle.circle(-args.tooth, 135)
turtle.forward(0.66*args.vertical_finger)
turtle.circle(-args.tooth, 135)
turtle.forward(slope)
turtle.circle(args.tooth, 135)
turtle.forward(ty-turtle.y)
def draw_case(x0, y0, h, slots):
global HOLES, SHAPES
turtle = svgturtle.SvgTurtle(x0, y0+args.thickness)
draw_case_h(turtle, h, True)
turtle.right(90)
draw_case_v(turtle, h)
turtle.right(90)
draw_case_h(turtle, h, False)
turtle.penup()
turtle.left(90)
turtle.back(h)
turtle.pendown()
draw_case_v(turtle, h)
SHAPES += '<path d="%s"/>\n' % turtle.to_s()
for slot in slots:
for side in range(5):
x = x0+(side+.5)*args.interior_edge+args.exterior_slot
y = y0+slot
w = args.horizontal_finger-args.kerf
h = args.thickness-args.kerf
HOLES += '<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f"/>\n' % (x, y, w, h)
print('<svg viewBox="0 0 %.2f %.2f" width="%.2fmm" height="%.2fmm" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">' % (DIMX, DIMY, DIMX, DIMY))
draw_grid(0.5*BOX, 0.5*BOX)
draw_plane(0.5*BOX, 0.5*BOX, 'interior')
draw_grid(1.5*BOX, 0.5*BOX)
draw_plane(1.5*BOX, 0.5*BOX, 'interior')
draw_plane(0.5*BOX, 1.5*BOX, 'bottom')
draw_plane(1.5*BOX, 1.5*BOX, 'rim')
draw_plane(1.5*BOX, 1.5*BOX, 'opening')
draw_disc(1.5*BOX, 1.5*BOX, 'disc')
draw_plane(2.5*BOX, 0.5*BOX, 'plug')
draw_plane(2.5*BOX, 1.5*BOX, 'lid')
draw_plane(2.5*BOX, 1.5*BOX, 'plug_mark')
draw_disc(2.5*BOX, 1.5*BOX, 'disc_mark')
draw_case(0.05*args.grid*args.dimension, 2.0*BOX, args.height+args.extra_height+args.thickness, args.slots)
print('<g fill="none" stroke="black">', SHAPES, '</g>', '<g fill="none" stroke="red">', HOLES, '</g>', '<g fill="none" stroke="blue">', MARKS, '</g>', sep='\n')
print('</svg>')

93
pincushion.py Executable file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import math
import sys
M=2
N=5
L=20.0
B=2.5
def frange(*args):
"""frange([start, ] end [, step [, mode]]) -> generator
A float range generator. If not specified, the default start is 0.0
and the default step is 1.0.
Optional argument mode sets whether frange outputs an open or closed
interval. mode must be an int. Bit zero of mode controls whether start is
included (on) or excluded (off); bit one does the same for end. Hence:
0 -> open interval (start and end both excluded)
1 -> half-open (start included, end excluded)
2 -> half open (start excluded, end included)
3 -> closed (start and end both included)
By default, mode=1 and only start is included in the output.
"""
mode = 1 # Default mode is half-open.
n = len(args)
if n == 1:
args = (0.0, args[0], 1.0)
elif n == 2:
args = args + (1.0,)
elif n == 4:
mode = args[3]
args = args[0:3]
elif n != 3:
raise TypeError('frange expects 1-4 arguments, got %d' % n)
assert len(args) == 3
try:
start, end, step = [a + 0.0 for a in args]
except TypeError:
raise TypeError('arguments must be numbers')
if step == 0.0:
raise ValueError('step must not be zero')
if not isinstance(mode, int):
raise TypeError('mode must be an int')
if mode & 1:
i, x = 0, start
else:
i, x = 1, start+step
if step > 0:
if mode & 2:
from operator import le as comp
else:
from operator import lt as comp
else:
if mode & 2:
from operator import ge as comp
else:
from operator import gt as comp
while comp(x, end):
yield x
i += 1
x = start + i*step
def cushion(x0, y0, hole, space):
yoff = .866*space # sqrt(3)/2
xodd = list(frange(x0+B, x0+L-B, space, 3))
xeven = list(frange(x0+B+.5*space, x0+L-B, space, 3))
row = 0
for y in frange(y0+B, y0+L-B, yoff, 3):
row += 1
if (row & 1) == 1:
xrow = xodd
else:
xrow = xeven
for x in xrow:
print('<circle cx="{}" cy="{}" r="{}" stroke="black" fill="none"/>'.format(x, y, hole))
print('<svg viewBox="-1 -1 101 41" width="102mm" height="42mm" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">')
print('<rect height="{}" width="{}" stroke="black" fill="none"/>'.format(M*L, N*L))
for i in range(1,M):
print('<line x1="0" y1="{}" x2="{}" y2="{}" stroke="red"/>'.format(i*L, N*L, i*L))
for j in range(1,N):
print('<line x1="{}" y1="0" x2="{}" y2="{}" stroke="red"/>'.format(j*L, j*L, M*L))
HOLE = [0.1, 0.05]
SPACE = [2.0, 1.5, 1.0, 0.75, 0.5]
for i in range(M):
for j in range(N):
cushion(j*L, i*L, HOLE[i], SPACE[j])
print('</svg>')

131
svgturtle.py Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""svgturtle - simple library to generate SVG paths from turtle graphics style commands"""
import math
import sys
class SvgTurtle():
def __init__(self, homex=0, homey=0):
"""Create a new turtle at the given home location, facing right"""
self.homex = homex
self.homey = homey
self.cvtangle = math.tau/360
self.reset()
def penup(self):
"""Subsequent movements will be invisible and only affect location and heading"""
self.pen = False
def pendown(self):
"""Subsequent movements will be visible and drawn"""
self.pen = True
def forward(self, distance):
"""Move forward"""
if self.pen and (self.path == ''):
self.path = "M %.2f,%.2f" % (self.x,self.y)
dx = distance*math.cos(self.heading)
dy = distance*math.sin(self.heading)
self.x += dx
self.y += dy
if self.pen:
if abs(dy) < .01:
self.path += " h %.2f" % dx
elif abs(dx) < .01:
self.path += " v %.2f" % dy
else:
self.path += " l %.2f,%.2f" % (dx, dy)
elif self.path != '':
self.path += " m %.2f, %.2f" % (dx, dy)
def back(self, distance):
"""Move backward"""
self.forward(-distance)
def left(self, angle):
"""Turn left by angle specified in degrees"""
self.right(-angle)
def right(self, angle):
"""Turn right by angle specified in degrees"""
self.heading = (self.heading + angle*self.cvtangle) % math.tau
def circle(self, radius, extent=360, steps=None):
"""Draw a circle or arc spanning extent degrees around a center
radius units to the left (if radius is positive) or right
(if radius is negative). Use a polygon if steps is specified,
otherwise a circle."""
if steps:
w = 1.0*extent/steps
w2 = 0.5*w
l = 2.0*radius*math.sin(w2*math.pi/180.0)
if radius < 0:
l, w, w2 = -l, -w, -w2
self.left(w2)
for i in range(steps):
self.forward(l)
self.left(w)
self.right(w2)
else:
if extent>355:
self.circle(radius, 355)
extent -= 355
ra = self.cvtangle*(extent if radius < 0 else -extent)
cx = self.x+radius*math.cos(self.heading-.5*math.pi)
cy = self.y+radius*math.sin(self.heading-.5*math.pi)
th = self.heading+.5*math.pi+ra
dx = cx+radius*math.cos(th)-self.x
dy = cy+radius*math.sin(th)-self.y
lg = 1 if extent >= 180 else 0
sw = 0 if radius > 0 else 1
if self.pen:
if self.path == '':
self.path = "M %.2f,%.2f" % (self.x,self.y)
self.path += " a %.2f %.2f %.2f %d %d %.2f %.2f" % (radius, radius, extent, lg, sw, dx, dy)
elif self.path != '':
self.path += " m %.2f, %.2f" % (dx, dy)
self.x += dx
self.y += dy
self.heading = (self.heading + ra) % math.tau
def to_s(self):
"""Return the generated path, suitable for the d attribute of an SVG <path> element"""
return self.path
def home(self):
"""Reset to the initial position and heading"""
self.x = self.homex
self.y = self.homey
self.heading = 0
if self.path != '':
self.path += " M %.2f, %.2f" % (self.x, self.y)
def reset(self):
"""Clear the path and return home"""
self.path = ''
self.pen = True
self.home()
if __name__ == "__main__":
turtle = SvgTurtle(50, 100)
turtle.left(90)
turtle.forward(50)
turtle.left(30)
turtle.forward(50)
turtle.penup()
turtle.back(50)
turtle.right(60)
turtle.pendown()
turtle.forward(50)
turtle.penup()
turtle.back(50)
turtle.left(30)
turtle.back(50)
turtle.right(90)
turtle.forward(70)
turtle.pendown()
turtle.circle(25)
print('<svg viewBox="0 0 170 110" xmlns="http://www.w3.org/2000/svg">')
print('<path fill="none" stroke="blue" d="%s"/>' % turtle.to_s())
print('</svg>')

43
svgturtletest.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import svgturtle
print('<svg viewBox="0 0 2000 2000" xmlns="http://www.w3.org/2000/svg">')
turtle_tl = svgturtle.SvgTurtle(500, 750)
turtle_tr = svgturtle.SvgTurtle(1500, 750)
turtle_bl = svgturtle.SvgTurtle(500, 1000)
turtle_br = svgturtle.SvgTurtle(1500, 1000)
ANGLE=0
for i in range(7):
turtle_tl.home()
turtle_tl.left(ANGLE)
turtle_tl.circle(110+i*50, (i+1)*45, 50)
turtle_bl.home()
turtle_bl.right(ANGLE)
turtle_bl.circle(-(110+i*50), (i+1)*45, 50)
turtle_tr.home()
turtle_tr.right(ANGLE)
turtle_tr.circle(140+i*50, (i+1)*45, 50)
turtle_br.home()
turtle_br.left(ANGLE)
turtle_br.circle(-(140+i*50), (i+1)*45, 50)
print('<path fill="none" stroke="black" d="%s %s %s %s"/>' % (turtle_tl.to_s(), turtle_tr.to_s(), turtle_bl.to_s(), turtle_br.to_s()))
turtle_tl.reset()
turtle_tr.reset()
turtle_bl.reset()
turtle_br.reset()
for i in range(8):
turtle_tl.home()
turtle_tl.left(ANGLE)
turtle_tl.circle(100+i*50, (i+1)*45)
turtle_bl.home()
turtle_bl.right(ANGLE)
turtle_bl.circle(-(100+i*50), (i+1)*45)
turtle_tr.home()
turtle_tr.right(ANGLE)
turtle_tr.circle(130+i*50, (i+1)*45)
turtle_br.home()
turtle_br.left(ANGLE)
turtle_br.circle(-(130+i*50), (i+1)*45)
print('<path fill="none" stroke="blue" d="%s %s %s %s"/>' % (turtle_tl.to_s(), turtle_tr.to_s(), turtle_bl.to_s(), turtle_br.to_s()))
print('</svg>')