FlavorParser.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. # Copyright (c) 2017 Ultimaker B.V.
  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.Message import Message
  10. from cura.Scene.CuraSceneNode import CuraSceneNode
  11. from UM.i18n import i18nCatalog
  12. from UM.Preferences import Preferences
  13. catalog = i18nCatalog("cura")
  14. from cura import LayerDataBuilder
  15. from cura.LayerDataDecorator import LayerDataDecorator
  16. from cura.LayerPolygon import LayerPolygon
  17. from cura.Scene.GCodeListDecorator import GCodeListDecorator
  18. from cura.Settings.ExtruderManager import ExtruderManager
  19. import numpy
  20. import math
  21. import re
  22. from collections import namedtuple
  23. # This parser is intented for interpret the common firmware codes among all the different flavors
  24. class FlavorParser:
  25. def __init__(self):
  26. Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
  27. self._cancelled = False
  28. self._message = None
  29. self._layer_number = 0
  30. self._extruder_number = 0
  31. self._clearValues()
  32. self._scene_node = None
  33. # X, Y, Z position, F feedrate and E extruder values are stored
  34. self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
  35. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  36. self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
  37. self._current_layer_thickness = 0.2 # default
  38. self._filament_diameter = 2.85 # default
  39. Preferences.getInstance().addPreference("gcodereader/show_caution", True)
  40. def _clearValues(self):
  41. self._extruder_number = 0
  42. self._extrusion_length_offset = [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. self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
  49. self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
  50. @staticmethod
  51. def _getValue(line, code):
  52. n = line.find(code)
  53. if n < 0:
  54. return None
  55. n += len(code)
  56. pattern = re.compile("[;\s]")
  57. match = pattern.search(line, n)
  58. m = match.start() if match is not None else -1
  59. try:
  60. if m < 0:
  61. return line[n:]
  62. return line[n:m]
  63. except:
  64. return None
  65. def _getInt(self, line, code):
  66. value = self._getValue(line, code)
  67. try:
  68. return int(value)
  69. except:
  70. return None
  71. def _getFloat(self, line, code):
  72. value = self._getValue(line, code)
  73. try:
  74. return float(value)
  75. except:
  76. return None
  77. def _onHideMessage(self, message):
  78. if message == self._message:
  79. self._cancelled = True
  80. @staticmethod
  81. def _getNullBoundingBox():
  82. return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
  83. def _createPolygon(self, layer_thickness, path, extruder_offsets):
  84. countvalid = 0
  85. for point in path:
  86. if point[5] > 0:
  87. countvalid += 1
  88. if countvalid >= 2:
  89. # we know what to do now, no need to count further
  90. continue
  91. if countvalid < 2:
  92. return False
  93. try:
  94. self._layer_data_builder.addLayer(self._layer_number)
  95. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  96. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  97. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  98. except ValueError:
  99. return False
  100. count = len(path)
  101. line_types = numpy.empty((count - 1, 1), numpy.int32)
  102. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  103. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  104. line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
  105. line_widths[:, 0] = 0.35 # Just a guess
  106. line_thicknesses[:, 0] = layer_thickness
  107. points = numpy.empty((count, 3), numpy.float32)
  108. extrusion_values = numpy.empty((count, 1), numpy.float32)
  109. i = 0
  110. for point in path:
  111. points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
  112. extrusion_values[i] = point[4]
  113. if i > 0:
  114. line_feedrates[i - 1] = point[3]
  115. line_types[i - 1] = point[5]
  116. if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  117. line_widths[i - 1] = 0.1
  118. line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
  119. else:
  120. line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
  121. i += 1
  122. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
  123. this_poly.buildCache()
  124. this_layer.polygons.append(this_poly)
  125. return True
  126. def _createEmptyLayer(self, layer_number):
  127. self._layer_data_builder.addLayer(layer_number)
  128. self._layer_data_builder.setLayerHeight(layer_number, 0)
  129. self._layer_data_builder.setLayerThickness(layer_number, 0)
  130. def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
  131. # Area of the filament
  132. Af = (self._filament_diameter / 2) ** 2 * numpy.pi
  133. # Length of the extruded filament
  134. de = current_extrusion - previous_extrusion
  135. # Volumne of the extruded filament
  136. dVe = de * Af
  137. # Length of the printed line
  138. dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
  139. # When the extruder recovers from a retraction, we get zero distance
  140. if dX == 0:
  141. return 0.1
  142. # Area of the printed line. This area is a rectangle
  143. Ae = dVe / dX
  144. # This area is a rectangle with area equal to layer_thickness * layer_width
  145. line_width = Ae / layer_thickness
  146. # A threshold is set to avoid weird paths in the GCode
  147. if line_width > 1.2:
  148. return 0.35
  149. return line_width
  150. def _gCode0(self, position, params, path):
  151. x, y, z, f, e = position
  152. if self._is_absolute_positioning:
  153. x = params.x if params.x is not None else x
  154. y = params.y if params.y is not None else y
  155. z = params.z if params.z is not None else z
  156. else:
  157. x += params.x if params.x is not None else 0
  158. y += params.y if params.y is not None else 0
  159. z += params.z if params.z is not None else 0
  160. f = params.f if params.f is not None else f
  161. if params.e is not None:
  162. new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
  163. if new_extrusion_value > e[self._extruder_number]:
  164. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
  165. else:
  166. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  167. e[self._extruder_number] = new_extrusion_value
  168. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  169. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  170. if z > self._previous_z and (z - self._previous_z < 1.5):
  171. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  172. self._previous_z = z
  173. else:
  174. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  175. return self._position(x, y, z, f, e)
  176. # G0 and G1 should be handled exactly the same.
  177. _gCode1 = _gCode0
  178. ## Home the head.
  179. def _gCode28(self, position, params, path):
  180. return self._position(
  181. params.x if params.x is not None else position.x,
  182. params.y if params.y is not None else position.y,
  183. params.z if params.z is not None else position.z,
  184. position.f,
  185. position.e)
  186. ## Set the absolute positioning
  187. def _gCode90(self, position, params, path):
  188. self._is_absolute_positioning = True
  189. self._is_absolute_extrusion = True
  190. return position
  191. ## Set the relative positioning
  192. def _gCode91(self, position, params, path):
  193. self._is_absolute_positioning = False
  194. self._is_absolute_extrusion = False
  195. return position
  196. ## Reset the current position to the values specified.
  197. # For example: G92 X10 will set the X to 10 without any physical motion.
  198. def _gCode92(self, position, params, path):
  199. if params.e is not None:
  200. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  201. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  202. position.e[self._extruder_number] = params.e
  203. return self._position(
  204. params.x if params.x is not None else position.x,
  205. params.y if params.y is not None else position.y,
  206. params.z if params.z is not None else position.z,
  207. params.f if params.f is not None else position.f,
  208. position.e)
  209. def processGCode(self, G, line, position, path):
  210. func = getattr(self, "_gCode%s" % G, None)
  211. line = line.split(";", 1)[0] # Remove comments (if any)
  212. if func is not None:
  213. s = line.upper().split(" ")
  214. x, y, z, f, e = None, None, None, None, None
  215. for item in s[1:]:
  216. if len(item) <= 1:
  217. continue
  218. if item.startswith(";"):
  219. continue
  220. if item[0] == "X":
  221. x = float(item[1:])
  222. if item[0] == "Y":
  223. y = float(item[1:])
  224. if item[0] == "Z":
  225. z = float(item[1:])
  226. if item[0] == "F":
  227. f = float(item[1:]) / 60
  228. if item[0] == "E":
  229. e = float(item[1:])
  230. if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
  231. self._center_is_zero = True
  232. params = self._position(x, y, z, f, e)
  233. return func(position, params, path)
  234. return position
  235. def processTCode(self, T, line, position, path):
  236. self._extruder_number = T
  237. if self._extruder_number + 1 > len(position.e):
  238. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  239. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  240. return position
  241. def processMCode(self, M, line, position, path):
  242. pass
  243. _type_keyword = ";TYPE:"
  244. _layer_keyword = ";LAYER:"
  245. ## For showing correct x, y offsets for each extruder
  246. def _extruderOffsets(self):
  247. result = {}
  248. for extruder in ExtruderManager.getInstance().getExtruderStacks():
  249. result[int(extruder.getMetaData().get("position", "0"))] = [
  250. extruder.getProperty("machine_nozzle_offset_x", "value"),
  251. extruder.getProperty("machine_nozzle_offset_y", "value")]
  252. return result
  253. def processGCodeStream(self, stream):
  254. Logger.log("d", "Preparing to load GCode")
  255. self._cancelled = False
  256. # We obtain the filament diameter from the selected extruder to calculate line widths
  257. global_stack = Application.getInstance().getGlobalContainerStack()
  258. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  259. scene_node = CuraSceneNode()
  260. # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
  261. # real data to calculate it from.
  262. scene_node.getBoundingBox = self._getNullBoundingBox
  263. gcode_list = []
  264. self._is_layers_in_file = False
  265. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  266. ##############################################################################################
  267. ## This part is where the action starts
  268. ##############################################################################################
  269. file_lines = 0
  270. current_line = 0
  271. for line in stream.split("\n"):
  272. file_lines += 1
  273. gcode_list.append(line + "\n")
  274. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  275. self._is_layers_in_file = True
  276. file_step = max(math.floor(file_lines / 100), 1)
  277. self._clearValues()
  278. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  279. lifetime=0,
  280. title = catalog.i18nc("@info:title", "G-code Details"))
  281. self._message.setProgress(0)
  282. self._message.show()
  283. Logger.log("d", "Parsing Gcode...")
  284. current_position = self._position(0, 0, 0, 0, [0])
  285. current_path = []
  286. min_layer_number = 0
  287. negative_layers = 0
  288. previous_layer = 0
  289. for line in stream.split("\n"):
  290. if self._cancelled:
  291. Logger.log("d", "Parsing Gcode file cancelled")
  292. return None
  293. current_line += 1
  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 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. # Start the new layer at the end position of the last layer
  322. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  323. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  324. # as in ProcessSlicedLayersJob
  325. if layer_number < min_layer_number:
  326. min_layer_number = layer_number
  327. if layer_number < 0:
  328. layer_number += abs(min_layer_number)
  329. negative_layers += 1
  330. else:
  331. layer_number += negative_layers
  332. # In case there is a gap in the layer count, empty layers are created
  333. for empty_layer in range(previous_layer + 1, layer_number):
  334. self._createEmptyLayer(empty_layer)
  335. self._layer_number = layer_number
  336. previous_layer = layer_number
  337. except:
  338. pass
  339. # This line is a comment. Ignore it (except for the layer_keyword)
  340. if line.startswith(";"):
  341. continue
  342. G = self._getInt(line, "G")
  343. if G is not None:
  344. # When find a movement, the new posistion is calculated and added to the current_path, but
  345. # don't need to create a polygon until the end of the layer
  346. current_position = self.processGCode(G, line, current_position, current_path)
  347. continue
  348. # When changing the extruder, the polygon with the stored paths is computed
  349. if line.startswith("T"):
  350. T = self._getInt(line, "T")
  351. if T is not None:
  352. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  353. current_path.clear()
  354. # When changing tool, store the end point of the previous path, then process the code and finally
  355. # add another point with the new position of the head.
  356. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  357. current_position = self.processTCode(T, line, current_position, current_path)
  358. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  359. if line.startswith("M"):
  360. M = self._getInt(line, "M")
  361. self.processMCode(M, line, current_position, current_path)
  362. # "Flush" leftovers. Last layer paths are still stored
  363. if len(current_path) > 1:
  364. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  365. self._layer_number += 1
  366. current_path.clear()
  367. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  368. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  369. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  370. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  371. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  372. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  373. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  374. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  375. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  376. layer_mesh = self._layer_data_builder.build(material_color_map)
  377. decorator = LayerDataDecorator()
  378. decorator.setLayerData(layer_mesh)
  379. scene_node.addDecorator(decorator)
  380. gcode_list_decorator = GCodeListDecorator()
  381. gcode_list_decorator.setGCodeList(gcode_list)
  382. scene_node.addDecorator(gcode_list_decorator)
  383. # gcode_dict stores gcode_lists for a number of build plates.
  384. active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  385. gcode_dict = {active_build_plate_id: gcode_list}
  386. Application.getInstance().getController().getScene().gcode_dict = gcode_dict
  387. Logger.log("d", "Finished parsing Gcode")
  388. self._message.hide()
  389. if self._layer_number == 0:
  390. Logger.log("w", "File doesn't contain any valid layers")
  391. settings = Application.getInstance().getGlobalContainerStack()
  392. machine_width = settings.getProperty("machine_width", "value")
  393. machine_depth = settings.getProperty("machine_depth", "value")
  394. if not self._center_is_zero:
  395. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  396. Logger.log("d", "GCode loading finished")
  397. if Preferences.getInstance().getValue("gcodereader/show_caution"):
  398. caution_message = Message(catalog.i18nc(
  399. "@info:generic",
  400. "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."),
  401. lifetime=0,
  402. title = catalog.i18nc("@info:title", "G-code Details"))
  403. caution_message.show()
  404. # The "save/print" button's state is bound to the backend state.
  405. backend = Application.getInstance().getBackend()
  406. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  407. return scene_node