FlavorParser.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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. self._previous_extrusion_value = 0.0 # keep track of the filament retractions
  41. CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
  42. def _clearValues(self) -> None:
  43. self._extruder_number = 0
  44. self._extrusion_length_offset = [0] # type: List[float]
  45. self._layer_type = LayerPolygon.Inset0Type
  46. self._layer_number = 0
  47. self._previous_z = 0 # type: float
  48. self._layer_data_builder = LayerDataBuilder()
  49. self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
  50. self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
  51. @staticmethod
  52. def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
  53. n = line.find(code)
  54. if n < 0:
  55. return None
  56. n += len(code)
  57. pattern = re.compile("[;\s]")
  58. match = pattern.search(line, n)
  59. m = match.start() if match is not None else -1
  60. try:
  61. if m < 0:
  62. return line[n:]
  63. return line[n:m]
  64. except:
  65. return None
  66. def _getInt(self, line: str, code: str) -> Optional[int]:
  67. value = self._getValue(line, code)
  68. try:
  69. return int(value) # type: ignore
  70. except:
  71. return None
  72. def _getFloat(self, line: str, code: str) -> Optional[float]:
  73. value = self._getValue(line, code)
  74. try:
  75. return float(value) # type: ignore
  76. except:
  77. return None
  78. def _onHideMessage(self, message: str) -> None:
  79. if message == self._message:
  80. self._cancelled = True
  81. def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
  82. countvalid = 0
  83. for point in path:
  84. if point[5] > 0:
  85. countvalid += 1
  86. if countvalid >= 2:
  87. # we know what to do now, no need to count further
  88. continue
  89. if countvalid < 2:
  90. return False
  91. try:
  92. self._layer_data_builder.addLayer(self._layer_number)
  93. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  94. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  95. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  96. except ValueError:
  97. return False
  98. count = len(path)
  99. line_types = numpy.empty((count - 1, 1), numpy.int32)
  100. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  101. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  102. line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
  103. line_widths[:, 0] = 0.35 # Just a guess
  104. line_thicknesses[:, 0] = layer_thickness
  105. points = numpy.empty((count, 3), numpy.float32)
  106. extrusion_values = numpy.empty((count, 1), numpy.float32)
  107. i = 0
  108. for point in path:
  109. points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
  110. extrusion_values[i] = point[4]
  111. if i > 0:
  112. line_feedrates[i - 1] = point[3]
  113. line_types[i - 1] = point[5]
  114. if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  115. line_widths[i - 1] = 0.1
  116. line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
  117. else:
  118. line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
  119. i += 1
  120. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
  121. this_poly.buildCache()
  122. this_layer.polygons.append(this_poly)
  123. return True
  124. def _createEmptyLayer(self, layer_number: int) -> None:
  125. self._layer_data_builder.addLayer(layer_number)
  126. self._layer_data_builder.setLayerHeight(layer_number, 0)
  127. self._layer_data_builder.setLayerThickness(layer_number, 0)
  128. def _calculateLineWidth(self, current_point: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
  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: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  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_extrusion else e[self._extruder_number] + params.e
  161. if new_extrusion_value > e[self._extruder_number]:
  162. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
  163. self._previous_extrusion_value = new_extrusion_value
  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. elif self._previous_extrusion_value > e[self._extruder_number]:
  173. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
  174. else:
  175. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  176. return self._position(x, y, z, f, e)
  177. # G0 and G1 should be handled exactly the same.
  178. _gCode1 = _gCode0
  179. ## Home the head.
  180. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  181. return self._position(
  182. params.x if params.x is not None else position.x,
  183. params.y if params.y is not None else position.y,
  184. params.z if params.z is not None else position.z,
  185. position.f,
  186. position.e)
  187. ## Set the absolute positioning
  188. def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  189. self._is_absolute_positioning = True
  190. self._is_absolute_extrusion = True
  191. return position
  192. ## Set the relative positioning
  193. def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  194. self._is_absolute_positioning = False
  195. self._is_absolute_extrusion = False
  196. return position
  197. ## Reset the current position to the values specified.
  198. # For example: G92 X10 will set the X to 10 without any physical motion.
  199. def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  200. if params.e is not None:
  201. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  202. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  203. position.e[self._extruder_number] = params.e
  204. self._previous_extrusion_value = params.e
  205. else:
  206. self._previous_extrusion_value = 0.0
  207. return self._position(
  208. params.x if params.x is not None else position.x,
  209. params.y if params.y is not None else position.y,
  210. params.z if params.z is not None else position.z,
  211. params.f if params.f is not None else position.f,
  212. position.e)
  213. def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  214. func = getattr(self, "_gCode%s" % G, None)
  215. line = line.split(";", 1)[0] # Remove comments (if any)
  216. if func is not None:
  217. s = line.upper().split(" ")
  218. x, y, z, f, e = None, None, None, None, None
  219. for item in s[1:]:
  220. if len(item) <= 1:
  221. continue
  222. if item.startswith(";"):
  223. continue
  224. if item[0] == "X":
  225. x = float(item[1:])
  226. if item[0] == "Y":
  227. y = float(item[1:])
  228. if item[0] == "Z":
  229. z = float(item[1:])
  230. if item[0] == "F":
  231. f = float(item[1:]) / 60
  232. if item[0] == "E":
  233. e = float(item[1:])
  234. params = PositionOptional(x, y, z, f, e)
  235. return func(position, params, path)
  236. return position
  237. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  238. self._extruder_number = T
  239. if self._extruder_number + 1 > len(position.e):
  240. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  241. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  242. return position
  243. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  244. pass
  245. _type_keyword = ";TYPE:"
  246. _layer_keyword = ";LAYER:"
  247. ## For showing correct x, y offsets for each extruder
  248. def _extruderOffsets(self) -> Dict[int, List[float]]:
  249. result = {}
  250. for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
  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 processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
  256. Logger.log("d", "Preparing to load GCode")
  257. self._cancelled = False
  258. # We obtain the filament diameter from the selected extruder to calculate line widths
  259. global_stack = CuraApplication.getInstance().getGlobalContainerStack()
  260. if not global_stack:
  261. return None
  262. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  263. scene_node = CuraSceneNode()
  264. gcode_list = []
  265. self._is_layers_in_file = False
  266. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  267. ##############################################################################################
  268. ## This part is where the action starts
  269. ##############################################################################################
  270. file_lines = 0
  271. current_line = 0
  272. for line in stream.split("\n"):
  273. file_lines += 1
  274. gcode_list.append(line + "\n")
  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_step = max(math.floor(file_lines / 100), 1)
  278. self._clearValues()
  279. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  280. lifetime=0,
  281. title = catalog.i18nc("@info:title", "G-code Details"))
  282. assert(self._message is not None) # use for typing purposes
  283. self._message.setProgress(0)
  284. self._message.show()
  285. Logger.log("d", "Parsing Gcode...")
  286. current_position = Position(0, 0, 0, 0, [0])
  287. current_path = [] #type: List[List[float]]
  288. min_layer_number = 0
  289. negative_layers = 0
  290. previous_layer = 0
  291. self._previous_extrusion_value = 0.0
  292. for line in stream.split("\n"):
  293. if self._cancelled:
  294. Logger.log("d", "Parsing Gcode file cancelled")
  295. return None
  296. current_line += 1
  297. if current_line % file_step == 0:
  298. self._message.setProgress(math.floor(current_line / file_lines * 100))
  299. Job.yieldThread()
  300. if len(line) == 0:
  301. continue
  302. if line.find(self._type_keyword) == 0:
  303. type = line[len(self._type_keyword):].strip()
  304. if type == "WALL-INNER":
  305. self._layer_type = LayerPolygon.InsetXType
  306. elif type == "WALL-OUTER":
  307. self._layer_type = LayerPolygon.Inset0Type
  308. elif type == "SKIN":
  309. self._layer_type = LayerPolygon.SkinType
  310. elif type == "SKIRT":
  311. self._layer_type = LayerPolygon.SkirtType
  312. elif type == "SUPPORT":
  313. self._layer_type = LayerPolygon.SupportType
  314. elif type == "FILL":
  315. self._layer_type = LayerPolygon.InfillType
  316. elif type == "SUPPORT-INTERFACE":
  317. self._layer_type = LayerPolygon.SupportInterfaceType
  318. else:
  319. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  320. # When the layer change is reached, the polygon is computed so we have just one layer per extruder
  321. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  322. try:
  323. layer_number = int(line[len(self._layer_keyword):])
  324. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  325. current_path.clear()
  326. # Start the new layer at the end position of the last layer
  327. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  328. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  329. # as in ProcessSlicedLayersJob
  330. if layer_number < min_layer_number:
  331. min_layer_number = layer_number
  332. if layer_number < 0:
  333. layer_number += abs(min_layer_number)
  334. negative_layers += 1
  335. else:
  336. layer_number += negative_layers
  337. # In case there is a gap in the layer count, empty layers are created
  338. for empty_layer in range(previous_layer + 1, layer_number):
  339. self._createEmptyLayer(empty_layer)
  340. self._layer_number = layer_number
  341. previous_layer = layer_number
  342. except:
  343. pass
  344. # This line is a comment. Ignore it (except for the layer_keyword)
  345. if line.startswith(";"):
  346. continue
  347. G = self._getInt(line, "G")
  348. if G is not None:
  349. # When find a movement, the new posistion is calculated and added to the current_path, but
  350. # don't need to create a polygon until the end of the layer
  351. current_position = self.processGCode(G, line, current_position, current_path)
  352. continue
  353. # When changing the extruder, the polygon with the stored paths is computed
  354. if line.startswith("T"):
  355. T = self._getInt(line, "T")
  356. if T is not None:
  357. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  358. current_path.clear()
  359. # When changing tool, store the end point of the previous path, then process the code and finally
  360. # add another point with the new position of the head.
  361. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  362. current_position = self.processTCode(T, line, current_position, current_path)
  363. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  364. if line.startswith("M"):
  365. M = self._getInt(line, "M")
  366. self.processMCode(M, line, current_position, current_path)
  367. # "Flush" leftovers. Last layer paths are still stored
  368. if len(current_path) > 1:
  369. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  370. self._layer_number += 1
  371. current_path.clear()
  372. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  373. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  374. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  375. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  376. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  377. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  378. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  379. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  380. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  381. layer_mesh = self._layer_data_builder.build(material_color_map)
  382. decorator = LayerDataDecorator()
  383. decorator.setLayerData(layer_mesh)
  384. scene_node.addDecorator(decorator)
  385. gcode_list_decorator = GCodeListDecorator()
  386. gcode_list_decorator.setGCodeList(gcode_list)
  387. scene_node.addDecorator(gcode_list_decorator)
  388. # gcode_dict stores gcode_lists for a number of build plates.
  389. active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  390. gcode_dict = {active_build_plate_id: gcode_list}
  391. CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
  392. Logger.log("d", "Finished parsing Gcode")
  393. self._message.hide()
  394. if self._layer_number == 0:
  395. Logger.log("w", "File doesn't contain any valid layers")
  396. settings = CuraApplication.getInstance().getGlobalContainerStack()
  397. if not settings.getProperty("machine_center_is_zero", "value"):
  398. machine_width = settings.getProperty("machine_width", "value")
  399. machine_depth = settings.getProperty("machine_depth", "value")
  400. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  401. Logger.log("d", "GCode loading finished")
  402. if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  403. caution_message = Message(catalog.i18nc(
  404. "@info:generic",
  405. "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."),
  406. lifetime=0,
  407. title = catalog.i18nc("@info:title", "G-code Details"))
  408. caution_message.show()
  409. # The "save/print" button's state is bound to the backend state.
  410. backend = CuraApplication.getInstance().getBackend()
  411. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  412. return scene_node