GCODE_EXPORT.PY 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import bpy, os
  2. import numpy as np
  3. import mathutils
  4. from mathutils import Vector
  5. from math import pi
  6. from bpy.types import (
  7. Operator,
  8. Panel,
  9. PropertyGroup,
  10. )
  11. from bpy.props import (
  12. BoolProperty,
  13. EnumProperty,
  14. FloatProperty,
  15. IntProperty,
  16. StringProperty,
  17. PointerProperty
  18. )
  19. from .utils import *
  20. def change_speed_mode(self, context):
  21. props = context.scene.tissue_gcode
  22. if props.previous_speed_mode != props.speed_mode:
  23. if props.speed_mode == 'SPEED':
  24. props.speed = props.feed/60
  25. props.speed_vertical = props.feed_vertical/60
  26. props.speed_horizontal = props.feed_horizontal/60
  27. else:
  28. props.feed = props.speed*60
  29. props.feed_vertical = props.speed_vertical*60
  30. props.feed_horizontal = props.speed_horizontal*60
  31. props.previous_speed_mode == props.speed_mode
  32. return
  33. class tissue_gcode_prop(PropertyGroup):
  34. last_e : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10)
  35. path_length : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10)
  36. folder : StringProperty(
  37. name="File", default="", subtype='FILE_PATH',
  38. description = 'Destination folder.\nIf missing, the file folder will be used'
  39. )
  40. pull : FloatProperty(
  41. name="Pull", default=5.0, min=0, soft_max=10,
  42. description='Pull material before lift'
  43. )
  44. push : FloatProperty(
  45. name="Push", default=5.0, min=0, soft_max=10,
  46. description='Push material before start extruding'
  47. )
  48. dz : FloatProperty(
  49. name="dz", default=2.0, min=0, soft_max=20,
  50. description='Z movement for lifting the nozzle before travel'
  51. )
  52. flow_mult : FloatProperty(
  53. name="Flow Mult", default=1.0, min=0, soft_max=3,
  54. description = 'Flow multiplier.\nUse a single value or a list of values for changing it during the printing path'
  55. )
  56. feed : IntProperty(
  57. name="Feed Rate (F)", default=3600, min=0, soft_max=20000,
  58. description='Printing speed'
  59. )
  60. feed_horizontal : IntProperty(
  61. name="Feed Horizontal", default=7200, min=0, soft_max=20000,
  62. description='Travel speed'
  63. )
  64. feed_vertical : IntProperty(
  65. name="Feed Vertical", default=3600, min=0, soft_max=20000,
  66. description='Lift movements speed'
  67. )
  68. speed : IntProperty(
  69. name="Speed", default=60, min=0, soft_max=100,
  70. description='Printing speed'
  71. )
  72. speed_horizontal : IntProperty(
  73. name="Travel", default=120, min=0, soft_max=200,
  74. description='Travel speed'
  75. )
  76. speed_vertical : IntProperty(
  77. name="Z-Lift", default=60, min=0, soft_max=200,
  78. description='Lift movements speed'
  79. )
  80. esteps : FloatProperty(
  81. name="E Steps/Unit", default=5, min=0, soft_max=100)
  82. start_code : StringProperty(
  83. name="Start", default='', description = 'Text block for starting code'
  84. )
  85. end_code : StringProperty(
  86. name="End", default='', description = 'Text block for ending code'
  87. )
  88. auto_sort_layers : BoolProperty(
  89. name="Auto Sort Layers", default=True,
  90. description = 'Sort layers according to the Z of the median point'
  91. )
  92. auto_sort_points : BoolProperty(
  93. name="Auto Sort Points", default=False,
  94. description = 'Shift layer points trying to automatically reduce needed travel movements'
  95. )
  96. close_all : BoolProperty(
  97. name="Close Shapes", default=False,
  98. description = 'Repeat the starting point at the end of the vertices list for each layer'
  99. )
  100. nozzle : FloatProperty(
  101. name="Nozzle", default=0.4, min=0, soft_max=10,
  102. description='Nozzle diameter'
  103. )
  104. layer_height : FloatProperty(
  105. name="Layer Height", default=0.1, min=0, soft_max=10,
  106. description = 'Average layer height, needed for a correct extrusion'
  107. )
  108. filament : FloatProperty(
  109. name="Filament (\u03A6)", default=1.75, min=0, soft_max=120,
  110. description='Filament (or material container) diameter'
  111. )
  112. gcode_mode : EnumProperty(items=[
  113. ("CONT", "Continuous", ""),
  114. ("RETR", "Retraction", "")
  115. ], default='CONT', name="Mode",
  116. description = 'If retraction is used, then each separated list of vertices\nwill be considered as a different layer'
  117. )
  118. speed_mode : EnumProperty(items=[
  119. ("SPEED", "Speed (mm/s)", ""),
  120. ("FEED", "Feed (mm/min)", "")
  121. ], default='SPEED', name="Speed Mode",
  122. description = 'Speed control mode',
  123. update = change_speed_mode
  124. )
  125. previous_speed_mode : StringProperty(
  126. name="previous_speed_mode", default='', description = ''
  127. )
  128. retraction_mode : EnumProperty(items=[
  129. ("FIRMWARE", "Firmware", ""),
  130. ("GCODE", "Gcode", "")
  131. ], default='GCODE', name="Retraction Mode",
  132. description = 'If firmware retraction is used, then the retraction parameters will be controlled by the printer'
  133. )
  134. animate : BoolProperty(
  135. name="Animate", default=False,
  136. description = 'Show print progression according to current frame'
  137. )
  138. class TISSUE_PT_gcode_exporter(Panel):
  139. bl_category = "Tissue Gcode"
  140. bl_space_type = "VIEW_3D"
  141. bl_region_type = "UI"
  142. #bl_space_type = 'PROPERTIES'
  143. #bl_region_type = 'WINDOW'
  144. #bl_context = "data"
  145. bl_label = "Tissue Gcode Export"
  146. #bl_options = {'DEFAULT_CLOSED'}
  147. @classmethod
  148. def poll(cls, context):
  149. try: return context.object.type in ('CURVE','MESH')
  150. except: return False
  151. def draw(self, context):
  152. props = context.scene.tissue_gcode
  153. #addon = context.user_preferences.addons.get(sverchok.__name__)
  154. #over_sized_buttons = addon.preferences.over_sized_buttons
  155. layout = self.layout
  156. col = layout.column(align=True)
  157. row = col.row()
  158. row.prop(props, 'folder', toggle=True, text='')
  159. col = layout.column(align=True)
  160. row = col.row()
  161. row.prop(props, 'gcode_mode', expand=True, toggle=True)
  162. #col = layout.column(align=True)
  163. col = layout.column(align=True)
  164. col.label(text="Extrusion:", icon='MOD_FLUIDSIM')
  165. #col.prop(self, 'esteps')
  166. col.prop(props, 'filament')
  167. col.prop(props, 'nozzle')
  168. col.prop(props, 'layer_height')
  169. col.separator()
  170. col.label(text="Speed (Feed Rate F):", icon='DRIVER')
  171. col.prop(props, 'speed_mode', text='')
  172. speed_prefix = 'feed' if props.speed_mode == 'FEED' else 'speed'
  173. col.prop(props, speed_prefix, text='Print')
  174. if props.gcode_mode == 'RETR':
  175. col.prop(props, speed_prefix + '_vertical', text='Z Lift')
  176. col.prop(props, speed_prefix + '_horizontal', text='Travel')
  177. col.separator()
  178. if props.gcode_mode == 'RETR':
  179. col = layout.column(align=True)
  180. col.label(text="Retraction Mode:", icon='NOCURVE')
  181. row = col.row()
  182. row.prop(props, 'retraction_mode', expand=True, toggle=True)
  183. if props.retraction_mode == 'GCODE':
  184. col.separator()
  185. col.label(text="Retraction:", icon='PREFERENCES')
  186. col.prop(props, 'pull', text='Retraction')
  187. col.prop(props, 'dz', text='Z Hop')
  188. col.prop(props, 'push', text='Preload')
  189. col.separator()
  190. #col.label(text="Layers options:", icon='ALIGN_JUSTIFY')
  191. col.separator()
  192. col.prop(props, 'auto_sort_layers', text="Sort Layers (Z)")
  193. col.prop(props, 'auto_sort_points', text="Sort Points (XY)")
  194. #col.prop(props, 'close_all')
  195. col.separator()
  196. col.label(text='Custom Code:', icon='TEXT')
  197. col.prop_search(props, 'start_code', bpy.data, 'texts')
  198. col.prop_search(props, 'end_code', bpy.data, 'texts')
  199. col.separator()
  200. row = col.row(align=True)
  201. row.scale_y = 2.0
  202. row.operator('scene.tissue_gcode_export')
  203. #col.separator()
  204. #col.prop(props, 'animate', icon='TIME')
  205. class tissue_gcode_export(Operator):
  206. bl_idname = "scene.tissue_gcode_export"
  207. bl_label = "Export Gcode"
  208. bl_description = ("Export selected curve object as Gcode file")
  209. bl_options = {'REGISTER', 'UNDO'}
  210. @classmethod
  211. def poll(cls, context):
  212. try:
  213. return context.object.type in ('CURVE', 'MESH')
  214. except:
  215. return False
  216. def execute(self, context):
  217. scene = context.scene
  218. props = scene.tissue_gcode
  219. # manage data
  220. if props.speed_mode == 'SPEED':
  221. props.feed = props.speed*60
  222. props.feed_vertical = props.speed_vertical*60
  223. props.feed_horizontal = props.speed_horizontal*60
  224. feed = props.feed
  225. feed_v = props.feed_vertical
  226. feed_h = props.feed_horizontal
  227. layer = props.layer_height
  228. flow_mult = props.flow_mult
  229. #if context.object.type != 'CURVE':
  230. # self.report({'ERROR'}, 'Please select a Curve object')
  231. # return {'CANCELLED'}
  232. ob = context.object
  233. matr = ob.matrix_world
  234. if ob.type == 'MESH':
  235. dg = context.evaluated_depsgraph_get()
  236. mesh = ob.evaluated_get(dg).data
  237. edges = [list(e.vertices) for e in mesh.edges]
  238. verts = [v.co for v in mesh.vertices]
  239. ordered_verts = find_curves(edges, len(mesh.vertices))
  240. ob = curve_from_pydata(verts, ordered_verts, name='__temp_curve__', merge_distance=0.1, set_active=False)
  241. vertices = [[matr @ p.co.xyz for p in s.points] for s in ob.data.splines]
  242. cyclic_u = [s.use_cyclic_u for s in ob.data.splines]
  243. if ob.name == '__temp_curve__': bpy.data.objects.remove(ob)
  244. if len(vertices) == 1: props.gcode_mode = 'CONT'
  245. export = True
  246. # open file
  247. if(export):
  248. if props.folder == '':
  249. folder = '//' + os.path.splitext(bpy.path.basename(bpy.context.blend_data.filepath))[0]
  250. else:
  251. folder = props.folder
  252. if '.gcode' not in folder: folder += '.gcode'
  253. path = bpy.path.abspath(folder)
  254. file = open(path, 'w')
  255. try:
  256. for line in bpy.data.texts[props.start_code].lines:
  257. file.write(line.body + '\n')
  258. except:
  259. pass
  260. #if props.gcode_mode == 'RETR':
  261. # sort layers (Z)
  262. if props.auto_sort_layers:
  263. sorted_verts = []
  264. for curve in vertices:
  265. # mean z
  266. listz = [v[2] for v in curve]
  267. meanz = np.mean(listz)
  268. # store curve and meanz
  269. sorted_verts.append((curve, meanz))
  270. vertices = [data[0] for data in sorted(sorted_verts, key=lambda height: height[1])]
  271. # sort vertices (XY)
  272. if props.auto_sort_points:
  273. # curves median point
  274. median_points = [np.mean(verts,axis=0) for verts in vertices]
  275. # chose starting point for each curve
  276. for j, curve in enumerate(vertices):
  277. # for closed curves finds the best starting point
  278. if cyclic_u[j]:
  279. # create kd tree
  280. kd = mathutils.kdtree.KDTree(len(curve))
  281. for i, v in enumerate(curve):
  282. kd.insert(v, i)
  283. kd.balance()
  284. if props.gcode_mode == 'RETR':
  285. if j==0:
  286. # close to next two curves median point
  287. co_find = np.mean(median_points[j+1:j+3],axis=0)
  288. elif j < len(vertices)-1:
  289. co_find = np.mean([median_points[j-1],median_points[j+1]],axis=0)
  290. else:
  291. co_find = np.mean(median_points[j-2:j],axis=0)
  292. #flow_mult[j] = flow_mult[j][index:]+flow_mult[j][:index]
  293. #layer[j] = layer[j][index:]+layer[j][:index]
  294. else:
  295. if j==0:
  296. # close to next two curves median point
  297. co_find = np.mean(median_points[j+1:j+3],axis=0)
  298. else:
  299. co_find = vertices[j-1][-1]
  300. co, index, dist = kd.find(co_find)
  301. vertices[j] = vertices[j][index:]+vertices[j][:index+1]
  302. else:
  303. if j > 0:
  304. p0 = curve[0]
  305. p1 = curve[-1]
  306. last = vertices[j-1][-1]
  307. d0 = (last-p0).length
  308. d1 = (last-p1).length
  309. if d1 < d0: vertices[j].reverse()
  310. '''
  311. # close shapes
  312. if props.close_all:
  313. for i in range(len(vertices)):
  314. vertices[i].append(vertices[i][0])
  315. #flow_mult[i].append(flow_mult[i][0])
  316. #layer[i].append(layer[i][0])
  317. '''
  318. # calc bounding box
  319. min_corner = np.min(vertices[0],axis=0)
  320. max_corner = np.max(vertices[0],axis=0)
  321. for i in range(1,len(vertices)):
  322. eval_points = vertices[i] + [min_corner]
  323. min_corner = np.min(eval_points,axis=0)
  324. eval_points = vertices[i] + [max_corner]
  325. max_corner = np.max(eval_points,axis=0)
  326. # initialize variables
  327. e = 0
  328. last_vert = Vector((0,0,0))
  329. maxz = 0
  330. path_length = 0
  331. travel_length = 0
  332. printed_verts = []
  333. printed_edges = []
  334. travel_verts = []
  335. travel_edges = []
  336. # write movements
  337. for i in range(len(vertices)):
  338. curve = vertices[i]
  339. first_id = len(printed_verts)
  340. for j in range(len(curve)):
  341. v = curve[j]
  342. v_flow_mult = flow_mult#[i][j]
  343. v_layer = layer#[i][j]
  344. # record max z
  345. maxz = np.max((maxz,v[2]))
  346. #maxz = max(maxz,v[2])
  347. # first point of the gcode
  348. if i == j == 0:
  349. printed_verts.append(v)
  350. if(export):
  351. file.write('G92 E0 \n')
  352. params = v[:3] + (feed,)
  353. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
  354. file.write(to_write)
  355. else:
  356. # start after retraction
  357. if j == 0 and props.gcode_mode == 'RETR':
  358. if(export):
  359. params = v[:2] + (maxz+props.dz,) + (feed_h,)
  360. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
  361. file.write(to_write)
  362. params = v[:3] + (feed_v,)
  363. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
  364. file.write(to_write)
  365. to_write = 'G1 F{:.0f}\n'.format(feed)
  366. file.write(to_write)
  367. if props.retraction_mode == 'GCODE':
  368. e += props.push
  369. file.write( 'G1 E' + format(e, '.4f') + '\n')
  370. else:
  371. file.write('G11\n')
  372. printed_verts.append((v[0], v[1], maxz+props.dz))
  373. travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
  374. travel_length += (Vector(printed_verts[-1])-Vector(printed_verts[-2])).length
  375. printed_verts.append(v)
  376. travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
  377. travel_length += maxz+props.dz - v[2]
  378. # regular extrusion
  379. else:
  380. printed_verts.append(v)
  381. v1 = Vector(v)
  382. v0 = Vector(curve[j-1])
  383. dist = (v1-v0).length
  384. area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle
  385. cylinder = pi*(props.filament/2)**2
  386. flow = area / cylinder * (0 if j == 0 else 1)
  387. e += dist * v_flow_mult * flow
  388. params = v[:3] + (e,)
  389. if(export):
  390. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params)
  391. file.write(to_write)
  392. path_length += dist
  393. printed_edges.append([len(printed_verts)-1, len(printed_verts)-2])
  394. if props.gcode_mode == 'RETR':
  395. v0 = Vector(curve[-1])
  396. if props.close_all and False:
  397. #printed_verts.append(v0)
  398. printed_edges.append([len(printed_verts)-1, first_id])
  399. v1 = Vector(curve[0])
  400. dist = (v0-v1).length
  401. area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle
  402. cylinder = pi*(props.filament/2)**2
  403. flow = area / cylinder
  404. e += dist * v_flow_mult * flow
  405. params = v1[:3] + (e,)
  406. if(export):
  407. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params)
  408. file.write(to_write)
  409. path_length += dist
  410. v0 = v1
  411. if i < len(vertices)-1:
  412. if(export):
  413. if props.retraction_mode == 'GCODE':
  414. e -= props.pull
  415. file.write('G0 E' + format(e, '.4f') + '\n')
  416. else:
  417. file.write('G10\n')
  418. params = v0[:2] + (maxz+props.dz,) + (feed_v,)
  419. to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
  420. file.write(to_write)
  421. printed_verts.append(v0.to_tuple())
  422. printed_verts.append((v0.x, v0.y, maxz+props.dz))
  423. travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
  424. travel_length += maxz+props.dz - v0.z
  425. if(export):
  426. # end code
  427. try:
  428. for line in bpy.data.texts[props.end_code].lines:
  429. file.write(line.body + '\n')
  430. except:
  431. pass
  432. file.close()
  433. print("Saved gcode to " + path)
  434. bb = list(min_corner) + list(max_corner)
  435. info = 'Bounding Box:\n'
  436. info += '\tmin\tX: {0:.1f}\tY: {1:.1f}\tZ: {2:.1f}\n'.format(*bb)
  437. info += '\tmax\tX: {3:.1f}\tY: {4:.1f}\tZ: {5:.1f}\n'.format(*bb)
  438. info += 'Extruded Filament: ' + format(e, '.2f') + '\n'
  439. info += 'Extruded Volume: ' + format(e*pi*(props.filament/2)**2, '.2f') + '\n'
  440. info += 'Printed Path Length: ' + format(path_length, '.2f') + '\n'
  441. info += 'Travel Length: ' + format(travel_length, '.2f')
  442. '''
  443. # animate
  444. if scene.animate:
  445. scene = bpy.context.scene
  446. try:
  447. param = (scene.frame_current - scene.frame_start)/(scene.frame_end - scene.frame_start)
  448. except:
  449. param = 1
  450. last_vert = max(int(param*len(printed_verts)),1)
  451. printed_verts = printed_verts[:last_vert]
  452. printed_edges = [e for e in printed_edges if e[0] < last_vert and e[1] < last_vert]
  453. travel_edges = [e for e in travel_edges if e[0] < last_vert and e[1] < last_vert]
  454. '''
  455. return {'FINISHED'}