GCodeReader.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # Copyright (c) 2016 Aleph Objects, Inc.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Application import Application
  4. from UM.Backend import Backend
  5. from UM.Job import Job
  6. from UM.Logger import Logger
  7. from UM.Math.AxisAlignedBox import AxisAlignedBox
  8. from UM.Math.Vector import Vector
  9. from UM.Mesh.MeshReader import MeshReader
  10. from UM.Message import Message
  11. from UM.Scene.SceneNode import SceneNode
  12. from UM.i18n import i18nCatalog
  13. from UM.Preferences import Preferences
  14. catalog = i18nCatalog("cura")
  15. from cura import LayerDataBuilder
  16. from cura import LayerDataDecorator
  17. from cura.LayerPolygon import LayerPolygon
  18. from cura.GCodeListDecorator import GCodeListDecorator
  19. from cura.Settings.ExtruderManager import ExtruderManager
  20. import numpy
  21. import math
  22. import re
  23. from collections import namedtuple
  24. # Class for loading and parsing G-code files
  25. class GCodeReader(MeshReader):
  26. def __init__(self):
  27. super(GCodeReader, self).__init__()
  28. self._supported_extensions = [".gcode", ".g"]
  29. Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
  30. self._cancelled = False
  31. self._message = None
  32. self._layer_number = 0
  33. self._extruder_number = 0
  34. self._clearValues()
  35. self._scene_node = None
  36. self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
  37. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  38. self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
  39. self._current_layer_thickness = 0.2 # default
  40. Preferences.getInstance().addPreference("gcodereader/show_caution", True)
  41. def _clearValues(self):
  42. self._extruder_number = 0
  43. self._layer_type = LayerPolygon.Inset0Type
  44. self._layer_number = 0
  45. self._previous_z = 0
  46. self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
  47. self._center_is_zero = False
  48. @staticmethod
  49. def _getValue(line, code):
  50. n = line.find(code)
  51. if n < 0:
  52. return None
  53. n += len(code)
  54. pattern = re.compile("[;\s]")
  55. match = pattern.search(line, n)
  56. m = match.start() if match is not None else -1
  57. try:
  58. if m < 0:
  59. return line[n:]
  60. return line[n:m]
  61. except:
  62. return None
  63. def _getInt(self, line, code):
  64. value = self._getValue(line, code)
  65. try:
  66. return int(value)
  67. except:
  68. return None
  69. def _getFloat(self, line, code):
  70. value = self._getValue(line, code)
  71. try:
  72. return float(value)
  73. except:
  74. return None
  75. def _onHideMessage(self, message):
  76. if message == self._message:
  77. self._cancelled = True
  78. @staticmethod
  79. def _getNullBoundingBox():
  80. return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
  81. def _createPolygon(self, layer_thickness, path, extruder_offsets):
  82. countvalid = 0
  83. for point in path:
  84. if point[3] > 0:
  85. countvalid += 1
  86. if countvalid >= 2:
  87. # we know what to do now, no need to count further
  88. continue
  89. if countvalid < 2:
  90. return False
  91. try:
  92. self._layer_data_builder.addLayer(self._layer_number)
  93. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  94. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  95. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  96. except ValueError:
  97. return False
  98. count = len(path)
  99. line_types = numpy.empty((count - 1, 1), numpy.int32)
  100. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  101. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  102. # TODO: need to calculate actual line width based on E values
  103. line_widths[:, 0] = 0.35 # Just a guess
  104. line_thicknesses[:, 0] = layer_thickness
  105. points = numpy.empty((count, 3), numpy.float32)
  106. i = 0
  107. for point in path:
  108. points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
  109. if i > 0:
  110. line_types[i - 1] = point[3]
  111. if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  112. line_widths[i - 1] = 0.1
  113. i += 1
  114. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses)
  115. this_poly.buildCache()
  116. this_layer.polygons.append(this_poly)
  117. return True
  118. def _gCode0(self, position, params, path):
  119. x, y, z, e = position
  120. x = params.x if params.x is not None else x
  121. y = params.y if params.y is not None else y
  122. z = params.z if params.z is not None else position.z
  123. if params.e is not None:
  124. if params.e > e[self._extruder_number]:
  125. path.append([x, y, z, self._layer_type]) # extrusion
  126. else:
  127. path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction
  128. e[self._extruder_number] = params.e
  129. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  130. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  131. if z > self._previous_z and (z - self._previous_z < 1.5):
  132. self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap
  133. self._previous_z = z
  134. else:
  135. path.append([x, y, z, LayerPolygon.MoveCombingType])
  136. return self._position(x, y, z, e)
  137. # G0 and G1 should be handled exactly the same.
  138. _gCode1 = _gCode0
  139. ## Home the head.
  140. def _gCode28(self, position, params, path):
  141. return self._position(
  142. params.x if params.x is not None else position.x,
  143. params.y if params.y is not None else position.y,
  144. 0,
  145. position.e)
  146. ## Reset the current position to the values specified.
  147. # For example: G92 X10 will set the X to 10 without any physical motion.
  148. def _gCode92(self, position, params, path):
  149. if params.e is not None:
  150. position.e[self._extruder_number] = params.e
  151. return self._position(
  152. params.x if params.x is not None else position.x,
  153. params.y if params.y is not None else position.y,
  154. params.z if params.z is not None else position.z,
  155. position.e)
  156. def _processGCode(self, G, line, position, path):
  157. func = getattr(self, "_gCode%s" % G, None)
  158. line = line.split(";", 1)[0] # Remove comments (if any)
  159. if func is not None:
  160. s = line.upper().split(" ")
  161. x, y, z, e = None, None, None, None
  162. for item in s[1:]:
  163. if len(item) <= 1:
  164. continue
  165. if item.startswith(";"):
  166. continue
  167. if item[0] == "X":
  168. x = float(item[1:])
  169. if item[0] == "Y":
  170. y = float(item[1:])
  171. if item[0] == "Z":
  172. z = float(item[1:])
  173. if item[0] == "E":
  174. e = float(item[1:])
  175. if (x is not None and x < 0) or (y is not None and y < 0):
  176. self._center_is_zero = True
  177. params = self._position(x, y, z, e)
  178. return func(position, params, path)
  179. return position
  180. def _processTCode(self, T, line, position, path):
  181. self._extruder_number = T
  182. if self._extruder_number + 1 > len(position.e):
  183. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  184. return position
  185. _type_keyword = ";TYPE:"
  186. _layer_keyword = ";LAYER:"
  187. ## For showing correct x, y offsets for each extruder
  188. def _extruderOffsets(self):
  189. result = {}
  190. for extruder in ExtruderManager.getInstance().getExtruderStacks():
  191. result[int(extruder.getMetaData().get("position", "0"))] = [
  192. extruder.getProperty("machine_nozzle_offset_x", "value"),
  193. extruder.getProperty("machine_nozzle_offset_y", "value")]
  194. return result
  195. def read(self, file_name):
  196. Logger.log("d", "Preparing to load %s" % file_name)
  197. self._cancelled = False
  198. scene_node = SceneNode()
  199. # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
  200. # real data to calculate it from.
  201. scene_node.getBoundingBox = self._getNullBoundingBox
  202. gcode_list = []
  203. self._is_layers_in_file = False
  204. Logger.log("d", "Opening file %s" % file_name)
  205. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  206. last_z = 0
  207. with open(file_name, "r") as file:
  208. file_lines = 0
  209. current_line = 0
  210. for line in file:
  211. file_lines += 1
  212. gcode_list.append(line)
  213. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  214. self._is_layers_in_file = True
  215. file.seek(0)
  216. file_step = max(math.floor(file_lines / 100), 1)
  217. self._clearValues()
  218. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0)
  219. self._message.setProgress(0)
  220. self._message.show()
  221. Logger.log("d", "Parsing %s..." % file_name)
  222. current_position = self._position(0, 0, 0, [0])
  223. current_path = []
  224. for line in file:
  225. if self._cancelled:
  226. Logger.log("d", "Parsing %s cancelled" % file_name)
  227. return None
  228. current_line += 1
  229. last_z = current_position.z
  230. if current_line % file_step == 0:
  231. self._message.setProgress(math.floor(current_line / file_lines * 100))
  232. Job.yieldThread()
  233. if len(line) == 0:
  234. continue
  235. if line.find(self._type_keyword) == 0:
  236. type = line[len(self._type_keyword):].strip()
  237. if type == "WALL-INNER":
  238. self._layer_type = LayerPolygon.InsetXType
  239. elif type == "WALL-OUTER":
  240. self._layer_type = LayerPolygon.Inset0Type
  241. elif type == "SKIN":
  242. self._layer_type = LayerPolygon.SkinType
  243. elif type == "SKIRT":
  244. self._layer_type = LayerPolygon.SkirtType
  245. elif type == "SUPPORT":
  246. self._layer_type = LayerPolygon.SupportType
  247. elif type == "FILL":
  248. self._layer_type = LayerPolygon.InfillType
  249. else:
  250. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  251. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  252. try:
  253. layer_number = int(line[len(self._layer_keyword):])
  254. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  255. current_path.clear()
  256. self._layer_number = layer_number
  257. except:
  258. pass
  259. # This line is a comment. Ignore it (except for the layer_keyword)
  260. if line.startswith(";"):
  261. continue
  262. G = self._getInt(line, "G")
  263. if G is not None:
  264. current_position = self._processGCode(G, line, current_position, current_path)
  265. # < 2 is a heuristic for a movement only, that should not be counted as a layer
  266. if current_position.z > last_z and abs(current_position.z - last_z) < 2:
  267. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  268. current_path.clear()
  269. if not self._is_layers_in_file:
  270. self._layer_number += 1
  271. continue
  272. if line.startswith("T"):
  273. T = self._getInt(line, "T")
  274. if T is not None:
  275. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  276. current_path.clear()
  277. current_position = self._processTCode(T, line, current_position, current_path)
  278. # "Flush" leftovers
  279. if not self._is_layers_in_file and len(current_path) > 1:
  280. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  281. self._layer_number += 1
  282. current_path.clear()
  283. material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
  284. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  285. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  286. layer_mesh = self._layer_data_builder.build(material_color_map)
  287. decorator = LayerDataDecorator.LayerDataDecorator()
  288. decorator.setLayerData(layer_mesh)
  289. scene_node.addDecorator(decorator)
  290. gcode_list_decorator = GCodeListDecorator()
  291. gcode_list_decorator.setGCodeList(gcode_list)
  292. scene_node.addDecorator(gcode_list_decorator)
  293. Application.getInstance().getController().getScene().gcode_list = gcode_list
  294. Logger.log("d", "Finished parsing %s" % file_name)
  295. self._message.hide()
  296. if self._layer_number == 0:
  297. Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
  298. settings = Application.getInstance().getGlobalContainerStack()
  299. machine_width = settings.getProperty("machine_width", "value")
  300. machine_depth = settings.getProperty("machine_depth", "value")
  301. if not self._center_is_zero:
  302. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  303. Logger.log("d", "Loaded %s" % file_name)
  304. if Preferences.getInstance().getValue("gcodereader/show_caution"):
  305. caution_message = Message(catalog.i18nc(
  306. "@info:generic",
  307. "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), lifetime=0)
  308. caution_message.show()
  309. # The "save/print" button's state is bound to the backend state.
  310. backend = Application.getInstance().getBackend()
  311. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  312. return scene_node