Exploratory scripts for laser cutter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
13 KiB

  1. #!/usr/bin/env python3
  2. import math
  3. import sys
  4. import argparse
  5. import svgturtle
  6. parser = argparse.ArgumentParser(description='Generate a hexagonal battery case')
  7. parser.add_argument('--dimension', default=5, type=int, help='Grid dimension')
  8. parser.add_argument('--height', type=float, help='Height of battery')
  9. parser.add_argument('--hole', default='AA', help='Hole diameter (mm or A, AA, AAA)')
  10. parser.add_argument('--kerf', default=.1, type=float, help='Kerf')
  11. parser.add_argument('--corner-length', default=4, help='Length of stretch corner in material thicknesses')
  12. parser.add_argument('--stretch', default=1.05, type=float, help='Reduction factor of stretch material')
  13. parser.add_argument('--horizontal-finger', default=5.0, type=float, help='Width of horizontal fingers')
  14. parser.add_argument('--vertical-finger', default=15.0, type=float, help='Width of vertical fingers')
  15. parser.add_argument('--padding', default=1.5, type=float, help='Padding around holes')
  16. parser.add_argument('--outside-padding', default=4.0, type=float, help='Extra padding between holes and wall')
  17. parser.add_argument('--extra-height', default=2.0, type=float, help='Extra vertical space')
  18. parser.add_argument('--thickness', default=3.0, type=float, help='Thickness of material')
  19. parser.add_argument('--lid', default=0.2, type=float, help='How much extra play to give the lid')
  20. parser.add_argument('--tooth', default=0.8, type=float, help='How much to round the edges of the teeth')
  21. parser.add_argument('--flex-width', default=.5, type=float, help='Spacing (in material thickness) between flex lines')
  22. parser.add_argument('--flex-cut', default=5.0, type=float, help='Length (in material thickness) of flex cuts')
  23. parser.add_argument('--flex-gap', default=1.0, type=float, help='Gap (in material thickness) between flex cuts')
  24. parser.add_argument('--plug-play', default=0.8, type=float, help='How much smaller to make the plug than the hole')
  25. parser.add_argument('--verbose', action='store_true', help='Print computed parameter values')
  26. args = parser.parse_args()
  27. assert (args.dimension % 2) == 1
  28. BATTERY = {
  29. 'AAA': [10.5, 44.5],
  30. 'AA': [14.5, 50.5],
  31. 'C': [26.2, 50.0],
  32. 'D': [34.2, 61.5],
  33. }
  34. if args.hole in BATTERY:
  35. dim = BATTERY[args.hole]
  36. args.hole = dim[0]
  37. if args.height == None:
  38. args.height = dim[1]
  39. else:
  40. args.hole = float(args.hole)
  41. assert(args.height != None)
  42. SUITABLE = False
  43. while not SUITABLE:
  44. args.kerf2 = args.kerf/2
  45. args.grid = args.hole+args.padding
  46. args.radius = args.hole/2
  47. args.corner = args.corner_length*args.thickness
  48. args.corner_s = args.corner*args.stretch
  49. args.corner_radius = 3*args.corner_s/math.pi
  50. args.plug_radius = args.corner_radius-args.thickness
  51. args.corner_inset = args.corner_s*math.sqrt(3)/math.pi
  52. args.plug_inset = args.plug_radius/math.sqrt(3)
  53. args.interior_edge = args.grid*args.dimension*0.5+args.outside_padding
  54. args.opening_edge = args.interior_edge-args.thickness
  55. args.plug_edge = min(args.opening_edge, .5*math.sqrt(3)*args.interior_edge)-args.plug_play
  56. args.disc_radius = 0.45*(math.sqrt(3)-1)*args.plug_edge+args.plug_inset
  57. args.exterior_edge = args.interior_edge+2.0*args.thickness
  58. args.interior_leg = (args.interior_edge-args.horizontal_finger-args.kerf)/2-args.corner_inset
  59. finger_length = args.interior_edge-2.0*args.corner_inset
  60. args.n_hor_fingers = max(int(finger_length/args.horizontal_finger/2), 1)
  61. args.exterior_leg = (finger_length-(2*args.n_hor_fingers-1)*args.horizontal_finger+args.kerf)/2
  62. args.wall_leg = (args.interior_edge-args.corner-(2*args.n_hor_fingers-1)*args.horizontal_finger+args.kerf)/2
  63. args.exterior_slot = (args.interior_edge-args.horizontal_finger+args.kerf)/2
  64. args.n_ver_fingers = int((args.height+args.extra_height)/args.vertical_finger)
  65. top_slot = args.extra_height+args.thickness+0.45*args.height
  66. args.slots = [top_slot, top_slot+15.0]
  67. if args.exterior_leg > 2:
  68. SUITABLE=True
  69. else:
  70. args.outside_padding += .5
  71. args.padding += .2 # Try again with more padding
  72. if args.verbose:
  73. print(args, file=sys.stderr)
  74. BOX = 2.0*args.exterior_edge+5.0
  75. DIMX = 3.0*BOX
  76. DIMY = 2.0*BOX+args.height+args.extra_height+3.0*args.thickness+5.0
  77. PI3 = math.pi/3
  78. HOLES = ''
  79. SHAPES = ''
  80. MARKS = ''
  81. def draw_grid(cx, cy):
  82. global HOLES
  83. for row in range(-int(args.dimension/2), int((args.dimension+1)/2)):
  84. cyr = cy+args.grid*row*math.sin(PI3)
  85. num_col = args.dimension-abs(row)
  86. cxr = cx-.5*args.grid*(num_col-1.0)
  87. for col in range(num_col):
  88. HOLES += '<circle cx="%.2f" cy="%.2f" r="%.2f"/>\n' % (cxr+col*args.grid, cyr, args.radius)
  89. def draw_disc(cx, cy, layer):
  90. global HOLES, SHAPES, MARKS
  91. turtle = svgturtle.SvgTurtle(cx, cy)
  92. turtle.penup()
  93. turtle.forward(args.disc_radius)
  94. turtle.pendown()
  95. turtle.left(90)
  96. turtle.circle(args.disc_radius)
  97. turtle.penup()
  98. turtle.home()
  99. if layer=='disc':
  100. HOLES += '<path d="%s"/>\n' % turtle.to_s()
  101. else:
  102. MARKS += '<path d="%s"/>\n' % turtle.to_s()
  103. def draw_plane(cx, cy, layer):
  104. global HOLES, SHAPES, MARKS
  105. if layer=='interior':
  106. edge = args.interior_edge
  107. elif layer=='opening':
  108. edge = args.opening_edge
  109. elif layer=='plug' or layer=='plug_mark':
  110. edge = args.plug_edge
  111. else:
  112. edge = args.exterior_edge
  113. turtle = svgturtle.SvgTurtle(cx, cy)
  114. turtle.penup()
  115. if layer=='plug_mark':
  116. turtle.right(30)
  117. turtle.forward(edge)
  118. turtle.right(120)
  119. if layer=='interior':
  120. turtle.forward(args.corner_inset)
  121. turtle.pendown()
  122. for side in range(5):
  123. turtle.forward(args.interior_leg)
  124. turtle.left(90)
  125. turtle.forward(args.thickness)
  126. turtle.right(90)
  127. turtle.forward(args.horizontal_finger+args.kerf)
  128. turtle.right(90)
  129. turtle.forward(args.thickness)
  130. turtle.left(90)
  131. turtle.forward(args.interior_leg)
  132. turtle.circle(-args.corner_radius, 60)
  133. turtle.forward(edge-2.0*args.corner_inset)
  134. turtle.circle(-args.corner_radius, 60)
  135. elif layer=='plug' or layer=='plug_mark':
  136. turtle.pendown()
  137. for side in range(3):
  138. turtle.forward(0.5*edge)
  139. turtle.right(90)
  140. turtle.circle(0.5*edge, 120)
  141. turtle.right(90)
  142. turtle.forward(0.5*edge)
  143. turtle.right(60)
  144. else:
  145. turtle.pendown()
  146. for side in range(6):
  147. turtle.forward(edge)
  148. turtle.right(60)
  149. if layer=='plug':
  150. HOLES += '<path d="%s"/>\n' % turtle.to_s()
  151. elif layer=='plug_mark':
  152. MARKS += '<path d="%s"/>\n' % turtle.to_s()
  153. else:
  154. SHAPES += '<path d="%s"/>\n' % turtle.to_s()
  155. turtle.reset()
  156. turtle.penup()
  157. if layer=='bottom' or layer=='rim':
  158. turtle.forward(args.interior_edge)
  159. turtle.right(120)
  160. turtle.forward(args.corner_inset)
  161. for side in range(5):
  162. for finger in range(args.n_hor_fingers):
  163. turtle.forward(args.exterior_leg if finger == 0 else args.horizontal_finger+args.kerf)
  164. turtle.pendown()
  165. turtle.left(90)
  166. turtle.forward(args.thickness)
  167. turtle.right(90)
  168. turtle.forward(args.horizontal_finger-args.kerf)
  169. turtle.right(90)
  170. turtle.forward(args.thickness)
  171. turtle.right(90)
  172. turtle.forward(args.horizontal_finger-args.kerf)
  173. turtle.penup()
  174. turtle.right(180)
  175. turtle.forward(args.horizontal_finger-args.kerf)
  176. turtle.forward(args.exterior_leg)
  177. turtle.pendown()
  178. turtle.left(90)
  179. turtle.forward(args.thickness)
  180. turtle.right(90)
  181. turtle.circle(-args.corner_radius-args.thickness, 60)
  182. turtle.right(90)
  183. turtle.forward(args.thickness)
  184. turtle.right(90)
  185. turtle.circle(args.corner_radius, 60)
  186. turtle.penup()
  187. turtle.right(180)
  188. turtle.circle(-args.corner_radius, 60)
  189. turtle.forward(args.interior_edge-2*args.corner_inset)
  190. turtle.pendown()
  191. turtle.left(90)
  192. turtle.forward(args.thickness)
  193. turtle.right(90)
  194. turtle.circle(-args.corner_radius-args.thickness, 60)
  195. turtle.right(90)
  196. turtle.forward(args.thickness)
  197. turtle.right(90)
  198. turtle.circle(args.corner_radius, 60)
  199. HOLES += '<path d="%s"/>\n' % turtle.to_s()
  200. def draw_flex(t, h):
  201. global HOLES
  202. turtle = svgturtle.SvgTurtle(t.x, t.y)
  203. gap = args.flex_gap*args.thickness
  204. ncut = max(int((h-gap) // (args.flex_cut*args.thickness)), 1)
  205. cut = ((h-gap) / ncut) - gap
  206. dx = args.flex_width*args.thickness
  207. nlines = int(args.corner // dx)
  208. x0 = .5*(args.corner - (nlines-1)*dx)
  209. turtle.forward(x0)
  210. for line in range(nlines):
  211. turtle.pendown()
  212. if (line % 2) == 0:
  213. turtle.right(90)
  214. turtle.forward(gap+cut)
  215. for section in range(ncut-2):
  216. turtle.penup()
  217. turtle.forward(gap)
  218. turtle.pendown()
  219. turtle.forward(gap+2*cut)
  220. turtle.penup()
  221. turtle.forward(gap)
  222. if (ncut % 2) == 0:
  223. turtle.pendown()
  224. turtle.forward(gap+cut)
  225. turtle.penup()
  226. turtle.left(90)
  227. else:
  228. turtle.left(90)
  229. if (ncut % 2) == 1:
  230. turtle.forward(gap+cut)
  231. for section in range(ncut-1-(ncut % 2)):
  232. turtle.penup()
  233. turtle.forward(gap)
  234. turtle.pendown()
  235. turtle.forward(gap+2*cut)
  236. turtle.penup()
  237. turtle.forward(gap)
  238. turtle.right(90)
  239. turtle.forward(dx)
  240. HOLES += '<path d="%s"/>\n' % turtle.to_s()
  241. def draw_case_h(turtle, h, top):
  242. turtle.forward(0.5*args.interior_edge-0.5*args.corner)
  243. for side in range(6):
  244. turtle.left(90)
  245. turtle.forward(args.thickness)
  246. turtle.right(90)
  247. if top:
  248. draw_flex(turtle, h+2*args.thickness)
  249. turtle.forward(args.corner)
  250. turtle.right(90)
  251. turtle.forward(args.thickness)
  252. turtle.left(90)
  253. if side == 5:
  254. break
  255. turtle.forward(args.wall_leg-0.5*args.kerf)
  256. for finger in range(args.n_hor_fingers):
  257. if finger > 0:
  258. turtle.forward(args.horizontal_finger-args.kerf)
  259. turtle.left(90)
  260. turtle.forward(args.thickness)
  261. turtle.right(90)
  262. turtle.forward(args.horizontal_finger+args.kerf)
  263. turtle.right(90)
  264. turtle.forward(args.thickness)
  265. turtle.left(90)
  266. turtle.forward(args.wall_leg-0.5*args.kerf)
  267. turtle.forward(0.5*args.interior_edge-0.5*args.corner)
  268. def draw_case_v(turtle, h):
  269. ey = turtle.y+h
  270. leg = (h-(args.n_ver_fingers-0.66)*args.vertical_finger)/2.0
  271. slope = args.vertical_finger/math.sqrt(18)
  272. turtle.forward(leg)
  273. for finger in range(args.n_ver_fingers):
  274. ty = ey if finger==args.n_ver_fingers-1 else turtle.y+args.vertical_finger
  275. turtle.circle(args.tooth, 135)
  276. turtle.forward(slope)
  277. turtle.circle(-args.tooth, 135)
  278. turtle.forward(0.66*args.vertical_finger)
  279. turtle.circle(-args.tooth, 135)
  280. turtle.forward(slope)
  281. turtle.circle(args.tooth, 135)
  282. turtle.forward(ty-turtle.y)
  283. def draw_case(x0, y0, h, slots):
  284. global HOLES, SHAPES
  285. turtle = svgturtle.SvgTurtle(x0, y0+args.thickness)
  286. draw_case_h(turtle, h, True)
  287. turtle.right(90)
  288. draw_case_v(turtle, h)
  289. turtle.right(90)
  290. draw_case_h(turtle, h, False)
  291. turtle.penup()
  292. turtle.left(90)
  293. turtle.back(h)
  294. turtle.pendown()
  295. draw_case_v(turtle, h)
  296. SHAPES += '<path d="%s"/>\n' % turtle.to_s()
  297. for slot in slots:
  298. for side in range(5):
  299. x = x0+(side+.5)*args.interior_edge+args.exterior_slot
  300. y = y0+slot
  301. w = args.horizontal_finger-args.kerf
  302. h = args.thickness-args.kerf
  303. HOLES += '<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f"/>\n' % (x, y, w, h)
  304. 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))
  305. draw_grid(0.5*BOX, 0.5*BOX)
  306. draw_plane(0.5*BOX, 0.5*BOX, 'interior')
  307. draw_grid(1.5*BOX, 0.5*BOX)
  308. draw_plane(1.5*BOX, 0.5*BOX, 'interior')
  309. draw_plane(0.5*BOX, 1.5*BOX, 'bottom')
  310. draw_plane(1.5*BOX, 1.5*BOX, 'rim')
  311. draw_plane(1.5*BOX, 1.5*BOX, 'opening')
  312. draw_disc(1.5*BOX, 1.5*BOX, 'disc')
  313. draw_plane(2.5*BOX, 0.5*BOX, 'plug')
  314. draw_plane(2.5*BOX, 1.5*BOX, 'lid')
  315. draw_plane(2.5*BOX, 1.5*BOX, 'plug_mark')
  316. draw_disc(2.5*BOX, 1.5*BOX, 'disc_mark')
  317. draw_case(0.05*args.grid*args.dimension, 2.0*BOX, args.height+args.extra_height+args.thickness, args.slots)
  318. print('<g fill="none" stroke="black">', SHAPES, '</g>', '<g fill="none" stroke="red">', HOLES, '</g>', '<g fill="none" stroke="blue">', MARKS, '</g>', sep='\n')
  319. print('</svg>')