FlavorParser.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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. Preferences.getInstance().addPreference("gcodereader/show_caution", True)
  39. def _clearValues(self):
  40. self._filament_diameter = 2.85
  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 processGCodeFile(self, file_name):
  254. Logger.log("d", "Preparing to load %s" % file_name)
  255. self._cancelled = False
  256. # We obtain the filament diameter from the selected printer to calculate line widths
  257. self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
  258. scene_node = CuraSceneNode()
  259. # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
  260. # real data to calculate it from.
  261. scene_node.getBoundingBox = self._getNullBoundingBox
  262. gcode_list = []
  263. self._is_layers_in_file = False
  264. Logger.log("d", "Opening file %s" % file_name)
  265. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  266. with open(file_name, "r") as file:
  267. file_lines = 0
  268. current_line = 0
  269. for line in file:
  270. file_lines += 1
  271. gcode_list.append(line)
  272. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  273. self._is_layers_in_file = True
  274. file.seek(0)
  275. file_step = max(math.floor(file_lines / 100), 1)
  276. self._clearValues()
  277. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  278. lifetime=0,
  279. title = catalog.i18nc("@info:title", "G-code Details"))
  280. self._message.setProgress(0)
  281. self._message.show()
  282. Logger.log("d", "Parsing %s..." % file_name)
  283. current_position = self._position(0, 0, 0, 0, [0])
  284. current_path = []
  285. min_layer_number = 0
  286. negative_layers = 0
  287. previous_layer = 0
  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. if current_line % file_step == 0:
  294. self._message.setProgress(math.floor(current_line / file_lines * 100))
  295. Job.yieldThread()
  296. if len(line) == 0:
  297. continue
  298. if line.find(self._type_keyword) == 0:
  299. type = line[len(self._type_keyword):].strip()
  300. if type == "WALL-INNER":
  301. self._layer_type = LayerPolygon.InsetXType
  302. elif type == "WALL-OUTER":
  303. self._layer_type = LayerPolygon.Inset0Type
  304. elif type == "SKIN":
  305. self._layer_type = LayerPolygon.SkinType
  306. elif type == "SKIRT":
  307. self._layer_type = LayerPolygon.SkirtType
  308. elif type == "SUPPORT":
  309. self._layer_type = LayerPolygon.SupportType
  310. elif type == "FILL":
  311. self._layer_type = LayerPolygon.InfillType
  312. else:
  313. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  314. # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
  315. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  316. try:
  317. layer_number = int(line[len(self._layer_keyword):])
  318. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  319. current_path.clear()
  320. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  321. # as in ProcessSlicedLayersJob
  322. if layer_number < min_layer_number:
  323. min_layer_number = layer_number
  324. if layer_number < 0:
  325. layer_number += abs(min_layer_number)
  326. negative_layers += 1
  327. else:
  328. layer_number += negative_layers
  329. # In case there is a gap in the layer count, empty layers are created
  330. for empty_layer in range(previous_layer + 1, layer_number):
  331. self._createEmptyLayer(empty_layer)
  332. self._layer_number = layer_number
  333. previous_layer = layer_number
  334. except:
  335. pass
  336. # This line is a comment. Ignore it (except for the layer_keyword)
  337. if line.startswith(";"):
  338. continue
  339. G = self._getInt(line, "G")
  340. if G is not None:
  341. # When find a movement, the new posistion is calculated and added to the current_path, but
  342. # don't need to create a polygon until the end of the layer
  343. current_position = self.processGCode(G, line, current_position, current_path)
  344. continue
  345. # When changing the extruder, the polygon with the stored paths is computed
  346. if line.startswith("T"):
  347. T = self._getInt(line, "T")
  348. if T is not None:
  349. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  350. current_path.clear()
  351. current_position = self.processTCode(T, line, current_position, current_path)
  352. if line.startswith("M"):
  353. M = self._getInt(line, "M")
  354. self.processMCode(M, line, current_position, current_path)
  355. # "Flush" leftovers. Last layer paths are still stored
  356. if len(current_path) > 1:
  357. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  358. self._layer_number += 1
  359. current_path.clear()
  360. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  361. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  362. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  363. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  364. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  365. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  366. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  367. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  368. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  369. layer_mesh = self._layer_data_builder.build(material_color_map)
  370. decorator = LayerDataDecorator()
  371. decorator.setLayerData(layer_mesh)
  372. scene_node.addDecorator(decorator)
  373. gcode_list_decorator = GCodeListDecorator()
  374. gcode_list_decorator.setGCodeList(gcode_list)
  375. scene_node.addDecorator(gcode_list_decorator)
  376. # gcode_dict stores gcode_lists for a number of build plates.
  377. active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
  378. gcode_dict = {active_build_plate_id: gcode_list}
  379. Application.getInstance().getController().getScene().gcode_dict = gcode_dict
  380. Logger.log("d", "Finished parsing %s" % file_name)
  381. self._message.hide()
  382. if self._layer_number == 0:
  383. Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
  384. settings = Application.getInstance().getGlobalContainerStack()
  385. machine_width = settings.getProperty("machine_width", "value")
  386. machine_depth = settings.getProperty("machine_depth", "value")
  387. if not self._center_is_zero:
  388. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  389. Logger.log("d", "Loaded %s" % file_name)
  390. if Preferences.getInstance().getValue("gcodereader/show_caution"):
  391. caution_message = Message(catalog.i18nc(
  392. "@info:generic",
  393. "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."),
  394. lifetime=0,
  395. title = catalog.i18nc("@info:title", "G-code Details"))
  396. caution_message.show()
  397. # The "save/print" button's state is bound to the backend state.
  398. backend = Application.getInstance().getBackend()
  399. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  400. return scene_node