GCodeReader.py 18 KB

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