Compare commits

...

13 Commits

3 changed files with 338 additions and 77 deletions

View File

@ -10,13 +10,21 @@ parser.add_argument('--dimension', default=5, type=int, help='Grid dimension')
parser.add_argument('--height', type=float, help='Height of battery') 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('--hole', default='AA', help='Hole diameter (mm or A, AA, AAA)')
parser.add_argument('--kerf', default=.1, type=float, help='Kerf') parser.add_argument('--kerf', default=.1, type=float, help='Kerf')
parser.add_argument('--horizontal-finger', default=10.0, type=float, help='Width of horizontal fingers') parser.add_argument('--corner-length', default=4, help='Length of stretch corner in material thicknesses')
parser.add_argument('--vertical-finger', default=5.0, type=float, help='Width of vertical fingers') 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('--padding', default=1.5, type=float, help='Padding around holes')
parser.add_argument('--outside-padding', default=2, type=float, help='Extra padding between holes and wall') 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('--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('--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() args = parser.parse_args()
assert (args.dimension % 2) == 1 assert (args.dimension % 2) == 1
@ -36,42 +44,93 @@ else:
args.hole = float(args.hole) args.hole = float(args.hole)
assert(args.height != None) assert(args.height != None)
args.kerf2 = args.kerf/2 SUITABLE = False
args.grid = args.hole+args.padding while not SUITABLE:
args.radius = args.hole/2 args.kerf2 = args.kerf/2
args.interior_edge = args.grid*args.dimension*0.5+args.outside_padding args.grid = args.hole+args.padding
args.exterior_edge = args.interior_edge+args.thickness args.radius = args.hole/2
args.interior_leg = (args.interior_edge-args.horizontal_finger-args.kerf)/2 args.corner = args.corner_length*args.thickness
args.n_hor_fingers = int(args.exterior_edge/args.horizontal_finger/2) args.corner_s = args.corner*args.stretch
args.exterior_leg = (args.exterior_edge-(2*args.n_hor_fingers-1)*args.horizontal_finger+args.kerf)/2 args.corner_radius = 3*args.corner_s/math.pi
args.exterior_slot = (args.exterior_edge-args.horizontal_finger+args.kerf)/2 args.plug_radius = args.corner_radius-args.thickness
args.n_ver_fingers = int(args.height/args.vertical_finger/2) args.corner_inset = args.corner_s*math.sqrt(3)/math.pi
args.vertical_finger = args.height/args.n_ver_fingers/2 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
DIMX = 2.9*args.grid*args.dimension if args.verbose:
DIMY = 3.8*args.grid*args.dimension 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 PI3 = math.pi/3
HOLES = ''
SHAPES = ''
MARKS = ''
def draw_grid(cx, cy): def draw_grid(cx, cy):
global HOLES
for row in range(-int(args.dimension/2), int((args.dimension+1)/2)): for row in range(-int(args.dimension/2), int((args.dimension+1)/2)):
cyr = cy+args.grid*row*math.sin(PI3) cyr = cy+args.grid*row*math.sin(PI3)
num_col = args.dimension-abs(row) num_col = args.dimension-abs(row)
cxr = cx-.5*args.grid*(num_col-1.0) cxr = cx-.5*args.grid*(num_col-1.0)
for col in range(num_col): for col in range(num_col):
print('<circle cx="%.2f" cy="%.2f" r="%.2f" stroke="black" fill="none"/>' % (cxr+col*args.grid, cyr, args.radius)) HOLES += '<circle cx="%.2f" cy="%.2f" r="%.2f"/>\n' % (cxr+col*args.grid, cyr, args.radius)
def draw_plane(cx, cy, interior=False): def draw_disc(cx, cy, layer):
if interior: 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 edge = args.interior_edge
elif layer=='opening':
edge = args.opening_edge
elif layer=='plug' or layer=='plug_mark':
edge = args.plug_edge
else: else:
edge = args.exterior_edge edge = args.exterior_edge
turtle = svgturtle.SvgTurtle(cx, cy) turtle = svgturtle.SvgTurtle(cx, cy)
turtle.penup() turtle.penup()
if layer=='plug_mark':
turtle.right(30)
turtle.forward(edge) turtle.forward(edge)
turtle.right(120) turtle.right(120)
if layer=='interior':
turtle.forward(args.corner_inset)
turtle.pendown() turtle.pendown()
if interior: for side in range(5):
for side in range(6):
turtle.forward(args.interior_leg) turtle.forward(args.interior_leg)
turtle.left(90) turtle.left(90)
turtle.forward(args.thickness) turtle.forward(args.thickness)
@ -81,75 +140,197 @@ def draw_plane(cx, cy, interior=False):
turtle.forward(args.thickness) turtle.forward(args.thickness)
turtle.left(90) turtle.left(90)
turtle.forward(args.interior_leg) 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) turtle.right(60)
else: else:
turtle.pendown()
for side in range(6): 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): for finger in range(args.n_hor_fingers):
turtle.forward(args.exterior_leg if finger == 0 else args.horizontal_finger+args.kerf) 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.right(90)
turtle.forward(args.thickness) turtle.forward(args.thickness)
turtle.left(90) 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.forward(args.horizontal_finger-args.kerf)
turtle.left(90) turtle.left(90)
turtle.forward(args.thickness) turtle.forward(args.thickness)
turtle.right(90) turtle.right(90)
turtle.forward(args.exterior_leg)
turtle.right(60)
print('<path d="%s" fill="none" stroke="black"/>' % turtle.to_s())
def draw_side(x0, y0, h, slots):
turtle = svgturtle.SvgTurtle(x0, y0+args.thickness)
for finger in range(args.n_hor_fingers):
turtle.forward(args.exterior_leg-args.kerf if finger == 0 else args.horizontal_finger-args.kerf)
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.horizontal_finger+args.kerf) turtle.forward(args.horizontal_finger+args.kerf)
turtle.right(90) turtle.right(90)
turtle.forward(args.thickness) turtle.forward(args.thickness)
turtle.left(90) turtle.left(90)
turtle.forward(args.exterior_leg-args.kerf-args.thickness) turtle.forward(args.wall_leg-0.5*args.kerf)
turtle.right(90) 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): for finger in range(args.n_ver_fingers):
turtle.forward(args.vertical_finger-(args.kerf/2 if finger==0 else args.kerf)) 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.left(90)
turtle.forward(args.thickness) turtle.back(h)
turtle.right(90) turtle.pendown()
turtle.forward(args.vertical_finger+args.kerf) draw_case_v(turtle, h)
turtle.right(90) SHAPES += '<path d="%s"/>\n' % turtle.to_s()
if finger < args.n_ver_fingers-1:
turtle.forward(args.thickness)
turtle.left(90)
turtle.forward(args.exterior_edge-args.thickness)
turtle.right(90)
for finger in range(args.n_ver_fingers):
turtle.forward(args.vertical_finger-(args.kerf/2 if finger==0 else args.kerf))
turtle.left(90)
turtle.forward(args.thickness)
turtle.right(90)
turtle.forward(args.vertical_finger+args.kerf)
turtle.right(90)
if finger < args.n_ver_fingers-1:
turtle.forward(args.thickness)
turtle.left(90)
print('<path d="%s" fill="none" stroke="black"/>' % turtle.to_s())
for slot in slots: for slot in slots:
x = x0+args.exterior_slot for side in range(5):
x = x0+(side+.5)*args.interior_edge+args.exterior_slot
y = y0+slot y = y0+slot
w = args.horizontal_finger-args.kerf w = args.horizontal_finger-args.kerf
h = args.thickness-args.kerf h = args.thickness-args.kerf
print('<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="none" stroke="black"/>' % (x, y, w, h)) 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)) 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.65*args.grid*args.dimension, 0.60*args.grid*args.dimension) draw_grid(0.5*BOX, 0.5*BOX)
draw_plane(0.65*args.grid*args.dimension, 0.60*args.grid*args.dimension, True) draw_plane(0.5*BOX, 0.5*BOX, 'interior')
draw_grid(1.90*args.grid*args.dimension, 0.60*args.grid*args.dimension) draw_grid(1.5*BOX, 0.5*BOX)
draw_plane(1.90*args.grid*args.dimension, 0.60*args.grid*args.dimension, True) draw_plane(1.5*BOX, 0.5*BOX, 'interior')
draw_plane(0.65*args.grid*args.dimension, 1.90*args.grid*args.dimension) draw_plane(0.5*BOX, 1.5*BOX, 'bottom')
draw_side(0.05*args.grid*args.dimension, 2.50*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) draw_plane(1.5*BOX, 1.5*BOX, 'rim')
draw_side(0.75*args.grid*args.dimension, 2.50*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) draw_plane(1.5*BOX, 1.5*BOX, 'opening')
draw_side(1.45*args.grid*args.dimension, 2.50*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) draw_disc(1.5*BOX, 1.5*BOX, 'disc')
draw_side(2.15*args.grid*args.dimension, 2.50*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) draw_plane(2.5*BOX, 0.5*BOX, 'plug')
draw_side(1.35*args.grid*args.dimension, 1.25*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) draw_plane(2.5*BOX, 1.5*BOX, 'lid')
draw_side(2.05*args.grid*args.dimension, 1.25*args.grid*args.dimension, args.height, [args.height*.3, args.height*.7]) 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>') print('</svg>')

View File

@ -40,6 +40,41 @@ class SvgTurtle():
def right(self, angle): def right(self, angle):
self.heading = (self.heading + angle*self.cvtangle) % math.tau self.heading = (self.heading + angle*self.cvtangle) % math.tau
def circle(self, radius, extent=360, steps=None):
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): def to_s(self):
return self.path return self.path
@ -47,6 +82,8 @@ class SvgTurtle():
self.x = self.homex self.x = self.homex
self.y = self.homey self.y = self.homey
self.heading = 0 self.heading = 0
if self.path != '':
self.path += " M %.2f, %.2f" % (self.x, self.y)
def reset(self): def reset(self):
self.path = '' self.path = ''

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