GCodeReader.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # Copyright (c) 2017 Aleph Objects, Inc.
  2. # Cura is released under the terms of the LGPLv3 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. # X, Y, Z position, F feedrate and E extruder values are stored
  37. self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
  38. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  39. self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
  40. self._current_layer_thickness = 0.2 # default
  41. Preferences.getInstance().addPreference("gcodereader/show_caution", True)
  42. def _clearValues(self):
  43. self._filament_diameter = 2.85
  44. self._extruder_number = 0
  45. self._extrusion_length_offset = [0]
  46. self._layer_type = LayerPolygon.Inset0Type
  47. self._layer_number = 0
  48. self._previous_z = 0
  49. self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
  50. self._center_is_zero = False
  51. self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
  52. self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
  53. @staticmethod
  54. def _getValue(line, code):
  55. n = line.find(code)
  56. if n < 0:
  57. return None
  58. n += len(code)
  59. pattern = re.compile("[;\s]")
  60. match = pattern.search(line, n)
  61. m = match.start() if match is not None else -1
  62. try:
  63. if m < 0:
  64. return line[n:]
  65. return line[n:m]
  66. except:
  67. return None
  68. def _getInt(self, line, code):
  69. value = self._getValue(line, code)
  70. try:
  71. return int(value)
  72. except:
  73. return None
  74. def _getFloat(self, line, code):
  75. value = self._getValue(line, code)
  76. try:
  77. return float(value)
  78. except:
  79. return None
  80. def _onHideMessage(self, message):
  81. if message == self._message:
  82. self._cancelled = True
  83. @staticmethod
  84. def _getNullBoundingBox():
  85. return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
  86. def _createPolygon(self, layer_thickness, path, extruder_offsets):
  87. countvalid = 0
  88. for point in path:
  89. if point[5] > 0:
  90. countvalid += 1
  91. if countvalid >= 2:
  92. # we know what to do now, no need to count further
  93. continue
  94. if countvalid < 2:
  95. return False
  96. try:
  97. self._layer_data_builder.addLayer(self._layer_number)
  98. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  99. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  100. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  101. except ValueError:
  102. return False
  103. count = len(path)
  104. line_types = numpy.empty((count - 1, 1), numpy.int32)
  105. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  106. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  107. line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
  108. line_widths[:, 0] = 0.35 # Just a guess
  109. line_thicknesses[:, 0] = layer_thickness
  110. points = numpy.empty((count, 3), numpy.float32)
  111. extrusion_values = numpy.empty((count, 1), numpy.float32)
  112. i = 0
  113. for point in path:
  114. points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
  115. extrusion_values[i] = point[4]
  116. if i > 0:
  117. line_feedrates[i - 1] = point[3]
  118. line_types[i - 1] = point[5]
  119. if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  120. line_widths[i - 1] = 0.1
  121. line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
  122. else:
  123. line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
  124. i += 1
  125. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
  126. this_poly.buildCache()
  127. this_layer.polygons.append(this_poly)
  128. return True
  129. def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
  130. # Area of the filament
  131. Af = (self._filament_diameter / 2) ** 2 * numpy.pi
  132. # Length of the extruded filament
  133. de = current_extrusion - previous_extrusion
  134. # Volumne of the extruded filament
  135. dVe = de * Af
  136. # Length of the printed line
  137. dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
  138. # When the extruder recovers from a retraction, we get zero distance
  139. if dX == 0:
  140. return 0.1
  141. # Area of the printed line. This area is a rectangle
  142. Ae = dVe / dX
  143. # This area is a rectangle with area equal to layer_thickness * layer_width
  144. line_width = Ae / layer_thickness
  145. # A threshold is set to avoid weird paths in the GCode
  146. if line_width > 1.2:
  147. return 0.35
  148. return line_width
  149. def _gCode0(self, position, params, path):
  150. x, y, z, f, e = position
  151. if self._is_absolute_positioning:
  152. x = params.x if params.x is not None else x
  153. y = params.y if params.y is not None else y
  154. z = params.z if params.z is not None else z
  155. else:
  156. x += params.x if params.x is not None else 0
  157. y += params.y if params.y is not None else 0
  158. z += params.z if params.z is not None else 0
  159. f = params.f if params.f is not None else f
  160. if params.e is not None:
  161. new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
  162. if new_extrusion_value > e[self._extruder_number]:
  163. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
  164. else:
  165. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  166. e[self._extruder_number] = new_extrusion_value
  167. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  168. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  169. if z > self._previous_z and (z - self._previous_z < 1.5):
  170. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  171. self._previous_z = z
  172. else:
  173. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  174. return self._position(x, y, z, f, e)
  175. # G0 and G1 should be handled exactly the same.
  176. _gCode1 = _gCode0
  177. ## Home the head.
  178. def _gCode28(self, position, params, path):
  179. return self._position(
  180. params.x if params.x is not None else position.x,
  181. params.y if params.y is not None else position.y,
  182. 0,
  183. position.f,
  184. position.e)
  185. ## Set the absolute positioning
  186. def _gCode90(self, position, params, path):
  187. self._is_absolute_positioning = True
  188. return position
  189. ## Set the relative positioning
  190. def _gCode91(self, position, params, path):
  191. self._is_absolute_positioning = False
  192. return position
  193. ## Reset the current position to the values specified.
  194. # For example: G92 X10 will set the X to 10 without any physical motion.
  195. def _gCode92(self, position, params, path):
  196. if params.e is not None:
  197. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  198. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  199. position.e[self._extruder_number] = params.e
  200. return self._position(
  201. params.x if params.x is not None else position.x,
  202. params.y if params.y is not None else position.y,
  203. params.z if params.z is not None else position.z,
  204. params.f if params.f is not None else position.f,
  205. position.e)
  206. def _processGCode(self, G, line, position, path):
  207. func = getattr(self, "_gCode%s" % G, None)
  208. line = line.split(";", 1)[0] # Remove comments (if any)
  209. if func is not None:
  210. s = line.upper().split(" ")
  211. x, y, z, f, e = None, None, None, None, None
  212. for item in s[1:]:
  213. if len(item) <= 1:
  214. continue
  215. if item.startswith(";"):
  216. continue
  217. if item[0] == "X":
  218. x = float(item[1:])
  219. if item[0] == "Y":
  220. y = float(item[1:])
  221. if item[0] == "Z":
  222. z = float(item[1:])
  223. if item[0] == "F":
  224. f = float(item[1:]) / 60
  225. if item[0] == "E":
  226. e = float(item[1:])
  227. if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
  228. self._center_is_zero = True
  229. params = self._position(x, y, z, f, e)
  230. return func(position, params, path)
  231. return position
  232. def _processTCode(self, T, line, position, path):
  233. self._extruder_number = T
  234. if self._extruder_number + 1 > len(position.e):
  235. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  236. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  237. return position
  238. def _processMCode(self, m):
  239. if m == 82:
  240. # Set absolute extrusion mode
  241. self._is_absolute_extrusion = True
  242. elif m == 83:
  243. # Set relative extrusion mode
  244. self._is_absolute_extrusion = False
  245. _type_keyword = ";TYPE:"
  246. _layer_keyword = ";LAYER:"
  247. ## For showing correct x, y offsets for each extruder
  248. def _extruderOffsets(self):
  249. result = {}
  250. for extruder in ExtruderManager.getInstance().getExtruderStacks():
  251. result[int(extruder.getMetaData().get("position", "0"))] = [
  252. extruder.getProperty("machine_nozzle_offset_x", "value"),
  253. extruder.getProperty("machine_nozzle_offset_y", "value")]
  254. return result
  255. def read(self, file_name):
  256. Logger.log("d", "Preparing to load %s" % file_name)
  257. self._cancelled = False
  258. # We obtain the filament diameter from the selected printer to calculate line widths
  259. self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
  260. scene_node = SceneNode()
  261. # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
  262. # real data to calculate it from.
  263. scene_node.getBoundingBox = self._getNullBoundingBox
  264. gcode_list = []
  265. self._is_layers_in_file = False
  266. Logger.log("d", "Opening file %s" % file_name)
  267. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  268. last_z = 0
  269. with open(file_name, "r") as file:
  270. file_lines = 0
  271. current_line = 0
  272. for line in file:
  273. file_lines += 1
  274. gcode_list.append(line)
  275. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  276. self._is_layers_in_file = True
  277. file.seek(0)
  278. file_step = max(math.floor(file_lines / 100), 1)
  279. self._clearValues()
  280. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  281. lifetime=0,
  282. title = catalog.i18nc("@info:title", "G-code Details"))
  283. self._message.setProgress(0)
  284. self._message.show()
  285. Logger.log("d", "Parsing %s..." % file_name)
  286. current_position = self._position(0, 0, 0, 0, [0])
  287. current_path = []
  288. for line in file:
  289. if self._cancelled:
  290. Logger.log("d", "Parsing %s cancelled" % file_name)
  291. return None
  292. current_line += 1
  293. last_z = current_position.z
  294. if current_line % file_step == 0:
  295. self._message.setProgress(math.floor(current_line / file_lines * 100))
  296. Job.yieldThread()
  297. if len(line) == 0:
  298. continue
  299. if line.find(self._type_keyword) == 0:
  300. type = line[len(self._type_keyword):].strip()
  301. if type == "WALL-INNER":
  302. self._layer_type = LayerPolygon.InsetXType
  303. elif type == "WALL-OUTER":
  304. self._layer_type = LayerPolygon.Inset0Type
  305. elif type == "SKIN":
  306. self._layer_type = LayerPolygon.SkinType
  307. elif type == "SKIRT":
  308. self._layer_type = LayerPolygon.SkirtType
  309. elif type == "SUPPORT":
  310. self._layer_type = LayerPolygon.SupportType
  311. elif type == "FILL":
  312. self._layer_type = LayerPolygon.InfillType
  313. else:
  314. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  315. # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
  316. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  317. try:
  318. layer_number = int(line[len(self._layer_keyword):])
  319. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  320. current_path.clear()
  321. self._layer_number = layer_number
  322. except:
  323. pass
  324. # This line is a comment. Ignore it (except for the layer_keyword)
  325. if line.startswith(";"):
  326. continue
  327. G = self._getInt(line, "G")
  328. if G is not None:
  329. # When find a movement, the new posistion is calculated and added to the current_path, but
  330. # don't need to create a polygon until the end of the layer
  331. current_position = self._processGCode(G, line, current_position, current_path)
  332. continue
  333. # When changing the extruder, the polygon with the stored paths is computed
  334. if line.startswith("T"):
  335. T = self._getInt(line, "T")
  336. if T is not None:
  337. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  338. current_path.clear()
  339. current_position = self._processTCode(T, line, current_position, current_path)
  340. if line.startswith("M"):
  341. M = self._getInt(line, "M")
  342. self._processMCode(M)
  343. # "Flush" leftovers. Last layer paths are still stored
  344. if len(current_path) > 1:
  345. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  346. self._layer_number += 1
  347. current_path.clear()
  348. material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
  349. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  350. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  351. layer_mesh = self._layer_data_builder.build(material_color_map)
  352. decorator = LayerDataDecorator.LayerDataDecorator()
  353. decorator.setLayerData(layer_mesh)
  354. scene_node.addDecorator(decorator)
  355. gcode_list_decorator = GCodeListDecorator()
  356. gcode_list_decorator.setGCodeList(gcode_list)
  357. scene_node.addDecorator(gcode_list_decorator)
  358. Application.getInstance().getController().getScene().gcode_list = gcode_list
  359. Logger.log("d", "Finished parsing %s" % file_name)
  360. self._message.hide()
  361. if self._layer_number == 0:
  362. Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
  363. settings = Application.getInstance().getGlobalContainerStack()
  364. machine_width = settings.getProperty("machine_width", "value")
  365. machine_depth = settings.getProperty("machine_depth", "value")
  366. if not self._center_is_zero:
  367. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  368. Logger.log("d", "Loaded %s" % file_name)
  369. if Preferences.getInstance().getValue("gcodereader/show_caution"):
  370. caution_message = Message(catalog.i18nc(
  371. "@info:generic",
  372. "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."),
  373. lifetime=0,
  374. title = catalog.i18nc("@info:title", "G-code Details"))
  375. caution_message.show()
  376. # The "save/print" button's state is bound to the backend state.
  377. backend = Application.getInstance().getBackend()
  378. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  379. return scene_node