FlavorParser.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. if not this_layer:
  97. return False
  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: int) -> None:
  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: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
  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: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  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. self._previous_extrusion_value = new_extrusion_value
  166. else:
  167. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  168. e[self._extruder_number] = new_extrusion_value
  169. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  170. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  171. if z > self._previous_z and (z - self._previous_z < 1.5):
  172. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  173. self._previous_z = z
  174. elif self._previous_extrusion_value > e[self._extruder_number]:
  175. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
  176. else:
  177. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  178. return self._position(x, y, z, f, e)
  179. # G0 and G1 should be handled exactly the same.
  180. _gCode1 = _gCode0
  181. ## Home the head.
  182. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  183. return self._position(
  184. params.x if params.x is not None else position.x,
  185. params.y if params.y is not None else position.y,
  186. params.z if params.z is not None else position.z,
  187. position.f,
  188. position.e)
  189. ## Set the absolute positioning
  190. def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  191. self._is_absolute_positioning = True
  192. self._is_absolute_extrusion = True
  193. return position
  194. ## Set the relative positioning
  195. def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  196. self._is_absolute_positioning = False
  197. self._is_absolute_extrusion = False
  198. return position
  199. ## Reset the current position to the values specified.
  200. # For example: G92 X10 will set the X to 10 without any physical motion.
  201. def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  202. if params.e is not None:
  203. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  204. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  205. position.e[self._extruder_number] = params.e
  206. self._previous_extrusion_value = params.e
  207. else:
  208. self._previous_extrusion_value = 0.0
  209. return self._position(
  210. params.x if params.x is not None else position.x,
  211. params.y if params.y is not None else position.y,
  212. params.z if params.z is not None else position.z,
  213. params.f if params.f is not None else position.f,
  214. position.e)
  215. def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  216. func = getattr(self, "_gCode%s" % G, None)
  217. line = line.split(";", 1)[0] # Remove comments (if any)
  218. if func is not None:
  219. s = line.upper().split(" ")
  220. x, y, z, f, e = None, None, None, None, None
  221. for item in s[1:]:
  222. if len(item) <= 1:
  223. continue
  224. if item.startswith(";"):
  225. continue
  226. if item[0] == "X":
  227. x = float(item[1:])
  228. if item[0] == "Y":
  229. y = float(item[1:])
  230. if item[0] == "Z":
  231. z = float(item[1:])
  232. if item[0] == "F":
  233. f = float(item[1:]) / 60
  234. if item[0] == "E":
  235. e = float(item[1:])
  236. params = PositionOptional(x, y, z, f, e)
  237. return func(position, params, path)
  238. return position
  239. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  240. self._extruder_number = T
  241. if self._extruder_number + 1 > len(position.e):
  242. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  243. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  244. return position
  245. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  246. pass
  247. _type_keyword = ";TYPE:"
  248. _layer_keyword = ";LAYER:"
  249. ## For showing correct x, y offsets for each extruder
  250. def _extruderOffsets(self) -> Dict[int, List[float]]:
  251. result = {}
  252. for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
  253. result[int(extruder.getMetaData().get("position", "0"))] = [
  254. extruder.getProperty("machine_nozzle_offset_x", "value"),
  255. extruder.getProperty("machine_nozzle_offset_y", "value")]
  256. return result
  257. def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
  258. Logger.log("d", "Preparing to load GCode")
  259. self._cancelled = False
  260. # We obtain the filament diameter from the selected extruder to calculate line widths
  261. global_stack = CuraApplication.getInstance().getGlobalContainerStack()
  262. if not global_stack:
  263. return None
  264. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  265. scene_node = CuraSceneNode()
  266. gcode_list = []
  267. self._is_layers_in_file = False
  268. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  269. ##############################################################################################
  270. ## This part is where the action starts
  271. ##############################################################################################
  272. file_lines = 0
  273. current_line = 0
  274. for line in stream.split("\n"):
  275. file_lines += 1
  276. gcode_list.append(line + "\n")
  277. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  278. self._is_layers_in_file = True
  279. file_step = max(math.floor(file_lines / 100), 1)
  280. self._clearValues()
  281. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  282. lifetime=0,
  283. title = catalog.i18nc("@info:title", "G-code Details"))
  284. assert(self._message is not None) # use for typing purposes
  285. self._message.setProgress(0)
  286. self._message.show()
  287. Logger.log("d", "Parsing Gcode...")
  288. current_position = Position(0, 0, 0, 0, [0])
  289. current_path = [] #type: List[List[float]]
  290. min_layer_number = 0
  291. negative_layers = 0
  292. previous_layer = 0
  293. self._previous_extrusion_value = 0.0
  294. for line in stream.split("\n"):
  295. if self._cancelled:
  296. Logger.log("d", "Parsing Gcode file cancelled")
  297. return None
  298. current_line += 1
  299. if current_line % file_step == 0:
  300. self._message.setProgress(math.floor(current_line / file_lines * 100))
  301. Job.yieldThread()
  302. if len(line) == 0:
  303. continue
  304. if line.find(self._type_keyword) == 0:
  305. type = line[len(self._type_keyword):].strip()
  306. if type == "WALL-INNER":
  307. self._layer_type = LayerPolygon.InsetXType
  308. elif type == "WALL-OUTER":
  309. self._layer_type = LayerPolygon.Inset0Type
  310. elif type == "SKIN":
  311. self._layer_type = LayerPolygon.SkinType
  312. elif type == "SKIRT":
  313. self._layer_type = LayerPolygon.SkirtType
  314. elif type == "SUPPORT":
  315. self._layer_type = LayerPolygon.SupportType
  316. elif type == "FILL":
  317. self._layer_type = LayerPolygon.InfillType
  318. elif type == "SUPPORT-INTERFACE":
  319. self._layer_type = LayerPolygon.SupportInterfaceType
  320. else:
  321. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  322. # When the layer change is reached, the polygon is computed so we have just one layer per extruder
  323. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  324. try:
  325. layer_number = int(line[len(self._layer_keyword):])
  326. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  327. current_path.clear()
  328. # Start the new layer at the end position of the last layer
  329. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  330. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  331. # as in ProcessSlicedLayersJob
  332. if layer_number < min_layer_number:
  333. min_layer_number = layer_number
  334. if layer_number < 0:
  335. layer_number += abs(min_layer_number)
  336. negative_layers += 1
  337. else:
  338. layer_number += negative_layers
  339. # In case there is a gap in the layer count, empty layers are created
  340. for empty_layer in range(previous_layer + 1, layer_number):
  341. self._createEmptyLayer(empty_layer)
  342. self._layer_number = layer_number
  343. previous_layer = layer_number
  344. except:
  345. pass
  346. # This line is a comment. Ignore it (except for the layer_keyword)
  347. if line.startswith(";"):
  348. continue
  349. G = self._getInt(line, "G")
  350. if G is not None:
  351. # When find a movement, the new posistion is calculated and added to the current_path, but
  352. # don't need to create a polygon until the end of the layer
  353. current_position = self.processGCode(G, line, current_position, current_path)
  354. continue
  355. # When changing the extruder, the polygon with the stored paths is computed
  356. if line.startswith("T"):
  357. T = self._getInt(line, "T")
  358. if T is not None:
  359. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  360. current_path.clear()
  361. # When changing tool, store the end point of the previous path, then process the code and finally
  362. # add another point with the new position of the head.
  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. current_position = self.processTCode(T, line, current_position, current_path)
  365. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  366. if line.startswith("M"):
  367. M = self._getInt(line, "M")
  368. self.processMCode(M, line, current_position, current_path)
  369. # "Flush" leftovers. Last layer paths are still stored
  370. if len(current_path) > 1:
  371. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  372. self._layer_number += 1
  373. current_path.clear()
  374. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  375. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  376. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  377. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  378. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  379. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  380. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  381. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  382. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  383. layer_mesh = self._layer_data_builder.build(material_color_map)
  384. decorator = LayerDataDecorator()
  385. decorator.setLayerData(layer_mesh)
  386. scene_node.addDecorator(decorator)
  387. gcode_list_decorator = GCodeListDecorator()
  388. gcode_list_decorator.setGCodeList(gcode_list)
  389. scene_node.addDecorator(gcode_list_decorator)
  390. # gcode_dict stores gcode_lists for a number of build plates.
  391. active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  392. gcode_dict = {active_build_plate_id: gcode_list}
  393. CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
  394. Logger.log("d", "Finished parsing Gcode")
  395. self._message.hide()
  396. if self._layer_number == 0:
  397. Logger.log("w", "File doesn't contain any valid layers")
  398. settings = CuraApplication.getInstance().getGlobalContainerStack()
  399. if not settings.getProperty("machine_center_is_zero", "value"):
  400. machine_width = settings.getProperty("machine_width", "value")
  401. machine_depth = settings.getProperty("machine_depth", "value")
  402. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  403. Logger.log("d", "GCode loading finished")
  404. if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  405. caution_message = Message(catalog.i18nc(
  406. "@info:generic",
  407. "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."),
  408. lifetime=0,
  409. title = catalog.i18nc("@info:title", "G-code Details"))
  410. caution_message.show()
  411. # The "save/print" button's state is bound to the backend state.
  412. backend = CuraApplication.getInstance().getBackend()
  413. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  414. return scene_node