FlavorParser.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Backend import Backend
  4. from UM.Job import Job
  5. from UM.Logger import Logger
  6. from UM.Math.Vector import Vector
  7. from UM.Message import Message
  8. from cura.Scene.CuraSceneNode import CuraSceneNode
  9. from UM.i18n import i18nCatalog
  10. catalog = i18nCatalog("cura")
  11. from cura.CuraApplication import CuraApplication
  12. from cura.LayerDataBuilder import LayerDataBuilder
  13. from cura.LayerDataDecorator import LayerDataDecorator
  14. from cura.LayerPolygon import LayerPolygon
  15. from cura.Scene.GCodeListDecorator import GCodeListDecorator
  16. from cura.Settings.ExtruderManager import ExtruderManager
  17. import numpy
  18. import math
  19. import re
  20. from typing import Dict, List, NamedTuple, Optional, Union
  21. PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
  22. Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
  23. ## This parser is intended to interpret the common firmware codes among all the
  24. # different flavors
  25. class FlavorParser:
  26. def __init__(self) -> None:
  27. CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
  28. self._cancelled = False
  29. self._message = None
  30. self._layer_number = 0
  31. self._extruder_number = 0
  32. self._clearValues()
  33. self._scene_node = None
  34. # X, Y, Z position, F feedrate and E extruder values are stored
  35. self._position = Position
  36. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  37. self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
  38. self._current_layer_thickness = 0.2 # default
  39. self._filament_diameter = 2.85 # default
  40. CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
  41. def _clearValues(self) -> None:
  42. self._extruder_number = 0
  43. self._extrusion_length_offset = [0] # type: List[float]
  44. self._layer_type = LayerPolygon.Inset0Type
  45. self._layer_number = 0
  46. self._previous_z = 0 # type: float
  47. self._layer_data_builder = LayerDataBuilder()
  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: str, code: str) -> Optional[Union[str, int, float]]:
  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: str, code: str) -> Optional[int]:
  66. value = self._getValue(line, code)
  67. try:
  68. return int(value) # type: ignore
  69. except:
  70. return None
  71. def _getFloat(self, line: str, code: str) -> Optional[float]:
  72. value = self._getValue(line, code)
  73. try:
  74. return float(value) # type: ignore
  75. except:
  76. return None
  77. def _onHideMessage(self, message: str) -> None:
  78. if message == self._message:
  79. self._cancelled = True
  80. def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
  81. countvalid = 0
  82. for point in path:
  83. if point[5] > 0:
  84. countvalid += 1
  85. if countvalid >= 2:
  86. # we know what to do now, no need to count further
  87. continue
  88. if countvalid < 2:
  89. return False
  90. try:
  91. self._layer_data_builder.addLayer(self._layer_number)
  92. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  93. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  94. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  95. except ValueError:
  96. return False
  97. count = len(path)
  98. line_types = numpy.empty((count - 1, 1), numpy.int32)
  99. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  100. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  101. line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
  102. line_widths[:, 0] = 0.35 # Just a guess
  103. line_thicknesses[:, 0] = layer_thickness
  104. points = numpy.empty((count, 3), numpy.float32)
  105. extrusion_values = numpy.empty((count, 1), 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. extrusion_values[i] = point[4]
  110. if i > 0:
  111. line_feedrates[i - 1] = point[3]
  112. line_types[i - 1] = point[5]
  113. if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  114. line_widths[i - 1] = 0.1
  115. line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
  116. else:
  117. line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
  118. i += 1
  119. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
  120. this_poly.buildCache()
  121. this_layer.polygons.append(this_poly)
  122. return True
  123. def _createEmptyLayer(self, layer_number: int) -> None:
  124. self._layer_data_builder.addLayer(layer_number)
  125. self._layer_data_builder.setLayerHeight(layer_number, 0)
  126. self._layer_data_builder.setLayerThickness(layer_number, 0)
  127. def _calculateLineWidth(self, current_point: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
  128. # Area of the filament
  129. Af = (self._filament_diameter / 2) ** 2 * numpy.pi
  130. # Length of the extruded filament
  131. de = current_extrusion - previous_extrusion
  132. # Volumne of the extruded filament
  133. dVe = de * Af
  134. # Length of the printed line
  135. dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
  136. # When the extruder recovers from a retraction, we get zero distance
  137. if dX == 0:
  138. return 0.1
  139. # Area of the printed line. This area is a rectangle
  140. Ae = dVe / dX
  141. # This area is a rectangle with area equal to layer_thickness * layer_width
  142. line_width = Ae / layer_thickness
  143. # A threshold is set to avoid weird paths in the GCode
  144. if line_width > 1.2:
  145. return 0.35
  146. return line_width
  147. def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  148. x, y, z, f, e = position
  149. if self._is_absolute_positioning:
  150. x = params.x if params.x is not None else x
  151. y = params.y if params.y is not None else y
  152. z = params.z if params.z is not None else z
  153. else:
  154. x += params.x if params.x is not None else 0
  155. y += params.y if params.y is not None else 0
  156. z += params.z if params.z is not None else 0
  157. f = params.f if params.f is not None else f
  158. if params.e is not None:
  159. new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
  160. if new_extrusion_value > e[self._extruder_number]:
  161. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
  162. else:
  163. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  164. e[self._extruder_number] = new_extrusion_value
  165. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  166. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  167. if z > self._previous_z and (z - self._previous_z < 1.5):
  168. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  169. self._previous_z = z
  170. else:
  171. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  172. return self._position(x, y, z, f, e)
  173. # G0 and G1 should be handled exactly the same.
  174. _gCode1 = _gCode0
  175. ## Home the head.
  176. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  177. return self._position(
  178. params.x if params.x is not None else position.x,
  179. params.y if params.y is not None else position.y,
  180. params.z if params.z is not None else position.z,
  181. position.f,
  182. position.e)
  183. ## Set the absolute positioning
  184. def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  185. self._is_absolute_positioning = True
  186. self._is_absolute_extrusion = True
  187. return position
  188. ## Set the relative positioning
  189. def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  190. self._is_absolute_positioning = False
  191. self._is_absolute_extrusion = 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: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  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: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  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. params = PositionOptional(x, y, z, f, e)
  228. return func(position, params, path)
  229. return position
  230. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  231. self._extruder_number = T
  232. if self._extruder_number + 1 > len(position.e):
  233. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  234. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  235. return position
  236. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  237. pass
  238. _type_keyword = ";TYPE:"
  239. _layer_keyword = ";LAYER:"
  240. ## For showing correct x, y offsets for each extruder
  241. def _extruderOffsets(self) -> Dict[int, List[float]]:
  242. result = {}
  243. for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
  244. result[int(extruder.getMetaData().get("position", "0"))] = [
  245. extruder.getProperty("machine_nozzle_offset_x", "value"),
  246. extruder.getProperty("machine_nozzle_offset_y", "value")]
  247. return result
  248. def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
  249. Logger.log("d", "Preparing to load GCode")
  250. self._cancelled = False
  251. # We obtain the filament diameter from the selected extruder to calculate line widths
  252. global_stack = CuraApplication.getInstance().getGlobalContainerStack()
  253. if not global_stack:
  254. return None
  255. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  256. scene_node = CuraSceneNode()
  257. gcode_list = []
  258. self._is_layers_in_file = False
  259. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  260. ##############################################################################################
  261. ## This part is where the action starts
  262. ##############################################################################################
  263. file_lines = 0
  264. current_line = 0
  265. for line in stream.split("\n"):
  266. file_lines += 1
  267. gcode_list.append(line + "\n")
  268. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  269. self._is_layers_in_file = True
  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. assert(self._message is not None) # use for typing purposes
  276. self._message.setProgress(0)
  277. self._message.show()
  278. Logger.log("d", "Parsing Gcode...")
  279. current_position = Position(0, 0, 0, 0, [0])
  280. current_path = [] #type: List[List[float]]
  281. min_layer_number = 0
  282. negative_layers = 0
  283. previous_layer = 0
  284. for line in stream.split("\n"):
  285. if self._cancelled:
  286. Logger.log("d", "Parsing Gcode file cancelled")
  287. return None
  288. current_line += 1
  289. if current_line % file_step == 0:
  290. self._message.setProgress(math.floor(current_line / file_lines * 100))
  291. Job.yieldThread()
  292. if len(line) == 0:
  293. continue
  294. if line.find(self._type_keyword) == 0:
  295. type = line[len(self._type_keyword):].strip()
  296. if type == "WALL-INNER":
  297. self._layer_type = LayerPolygon.InsetXType
  298. elif type == "WALL-OUTER":
  299. self._layer_type = LayerPolygon.Inset0Type
  300. elif type == "SKIN":
  301. self._layer_type = LayerPolygon.SkinType
  302. elif type == "SKIRT":
  303. self._layer_type = LayerPolygon.SkirtType
  304. elif type == "SUPPORT":
  305. self._layer_type = LayerPolygon.SupportType
  306. elif type == "FILL":
  307. self._layer_type = LayerPolygon.InfillType
  308. else:
  309. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  310. # When the layer change is reached, the polygon is computed so we have just one layer per extruder
  311. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  312. try:
  313. layer_number = int(line[len(self._layer_keyword):])
  314. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  315. current_path.clear()
  316. # Start the new layer at the end position of the last layer
  317. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  318. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  319. # as in ProcessSlicedLayersJob
  320. if layer_number < min_layer_number:
  321. min_layer_number = layer_number
  322. if layer_number < 0:
  323. layer_number += abs(min_layer_number)
  324. negative_layers += 1
  325. else:
  326. layer_number += negative_layers
  327. # In case there is a gap in the layer count, empty layers are created
  328. for empty_layer in range(previous_layer + 1, layer_number):
  329. self._createEmptyLayer(empty_layer)
  330. self._layer_number = layer_number
  331. previous_layer = layer_number
  332. except:
  333. pass
  334. # This line is a comment. Ignore it (except for the layer_keyword)
  335. if line.startswith(";"):
  336. continue
  337. G = self._getInt(line, "G")
  338. if G is not None:
  339. # When find a movement, the new posistion is calculated and added to the current_path, but
  340. # don't need to create a polygon until the end of the layer
  341. current_position = self.processGCode(G, line, current_position, current_path)
  342. continue
  343. # When changing the extruder, the polygon with the stored paths is computed
  344. if line.startswith("T"):
  345. T = self._getInt(line, "T")
  346. if T is not None:
  347. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  348. current_path.clear()
  349. # When changing tool, store the end point of the previous path, then process the code and finally
  350. # add another point with the new position of the head.
  351. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  352. current_position = self.processTCode(T, line, current_position, current_path)
  353. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  354. if line.startswith("M"):
  355. M = self._getInt(line, "M")
  356. self.processMCode(M, line, current_position, current_path)
  357. # "Flush" leftovers. Last layer paths are still stored
  358. if len(current_path) > 1:
  359. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  360. self._layer_number += 1
  361. current_path.clear()
  362. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  363. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  364. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  365. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  366. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  367. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  368. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  369. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  370. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  371. layer_mesh = self._layer_data_builder.build(material_color_map)
  372. decorator = LayerDataDecorator()
  373. decorator.setLayerData(layer_mesh)
  374. scene_node.addDecorator(decorator)
  375. gcode_list_decorator = GCodeListDecorator()
  376. gcode_list_decorator.setGCodeList(gcode_list)
  377. scene_node.addDecorator(gcode_list_decorator)
  378. # gcode_dict stores gcode_lists for a number of build plates.
  379. active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  380. gcode_dict = {active_build_plate_id: gcode_list}
  381. CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
  382. Logger.log("d", "Finished parsing Gcode")
  383. self._message.hide()
  384. if self._layer_number == 0:
  385. Logger.log("w", "File doesn't contain any valid layers")
  386. settings = CuraApplication.getInstance().getGlobalContainerStack()
  387. if not settings.getProperty("machine_center_is_zero", "value"):
  388. machine_width = settings.getProperty("machine_width", "value")
  389. machine_depth = settings.getProperty("machine_depth", "value")
  390. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  391. Logger.log("d", "GCode loading finished")
  392. if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  393. caution_message = Message(catalog.i18nc(
  394. "@info:generic",
  395. "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."),
  396. lifetime=0,
  397. title = catalog.i18nc("@info:title", "G-code Details"))
  398. caution_message.show()
  399. # The "save/print" button's state is bound to the backend state.
  400. backend = CuraApplication.getInstance().getBackend()
  401. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  402. return scene_node