FlavorParser.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. # Copyright (c) 2018 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. catalog = i18nCatalog("cura")
  13. from cura import LayerDataBuilder
  14. from cura.LayerDataDecorator import LayerDataDecorator
  15. from cura.LayerPolygon import LayerPolygon
  16. from cura.Scene.GCodeListDecorator import GCodeListDecorator
  17. from cura.Settings.ExtruderManager import ExtruderManager
  18. import numpy
  19. import math
  20. import re
  21. from typing import Dict, List, NamedTuple, Optional, Union
  22. from collections import namedtuple
  23. Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", float)])
  24. ## This parser is intended to interpret the common firmware codes among all the
  25. # different flavors
  26. class FlavorParser:
  27. def __init__(self) -> None:
  28. Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
  29. self._cancelled = False
  30. self._message = None
  31. self._layer_number = 0
  32. self._extruder_number = 0
  33. self._clearValues()
  34. self._scene_node = None
  35. # X, Y, Z position, F feedrate and E extruder values are stored
  36. self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
  37. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  38. self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
  39. self._current_layer_thickness = 0.2 # default
  40. self._filament_diameter = 2.85 # default
  41. Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
  42. def _clearValues(self) -> None:
  43. self._extruder_number = 0
  44. self._extrusion_length_offset = [0]
  45. self._layer_type = LayerPolygon.Inset0Type
  46. self._layer_number = 0
  47. self._previous_z = 0
  48. self._layer_data_builder = LayerDataBuilder.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)
  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)
  76. except:
  77. return None
  78. def _onHideMessage(self, message: str) -> None:
  79. if message == self._message:
  80. self._cancelled = True
  81. @staticmethod
  82. def _getNullBoundingBox() -> AxisAlignedBox:
  83. return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
  84. def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
  85. countvalid = 0
  86. for point in path:
  87. if point[5] > 0:
  88. countvalid += 1
  89. if countvalid >= 2:
  90. # we know what to do now, no need to count further
  91. continue
  92. if countvalid < 2:
  93. return False
  94. try:
  95. self._layer_data_builder.addLayer(self._layer_number)
  96. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  97. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  98. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  99. except ValueError:
  100. return False
  101. count = len(path)
  102. line_types = numpy.empty((count - 1, 1), numpy.int32)
  103. line_widths = numpy.empty((count - 1, 1), numpy.float32)
  104. line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
  105. line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
  106. line_widths[:, 0] = 0.35 # Just a guess
  107. line_thicknesses[:, 0] = layer_thickness
  108. points = numpy.empty((count, 3), numpy.float32)
  109. extrusion_values = numpy.empty((count, 1), numpy.float32)
  110. i = 0
  111. for point in path:
  112. points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
  113. extrusion_values[i] = point[4]
  114. if i > 0:
  115. line_feedrates[i - 1] = point[3]
  116. line_types[i - 1] = point[5]
  117. if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
  118. line_widths[i - 1] = 0.1
  119. line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
  120. else:
  121. line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
  122. i += 1
  123. this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
  124. this_poly.buildCache()
  125. this_layer.polygons.append(this_poly)
  126. return True
  127. def _createEmptyLayer(self, layer_number: int) -> None:
  128. self._layer_data_builder.addLayer(layer_number)
  129. self._layer_data_builder.setLayerHeight(layer_number, 0)
  130. self._layer_data_builder.setLayerThickness(layer_number, 0)
  131. def _calculateLineWidth(self, current_point: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
  132. # Area of the filament
  133. Af = (self._filament_diameter / 2) ** 2 * numpy.pi
  134. # Length of the extruded filament
  135. de = current_extrusion - previous_extrusion
  136. # Volumne of the extruded filament
  137. dVe = de * Af
  138. # Length of the printed line
  139. dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
  140. # When the extruder recovers from a retraction, we get zero distance
  141. if dX == 0:
  142. return 0.1
  143. # Area of the printed line. This area is a rectangle
  144. Ae = dVe / dX
  145. # This area is a rectangle with area equal to layer_thickness * layer_width
  146. line_width = Ae / layer_thickness
  147. # A threshold is set to avoid weird paths in the GCode
  148. if line_width > 1.2:
  149. return 0.35
  150. return line_width
  151. def _gCode0(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
  152. x, y, z, f, e = position
  153. if self._is_absolute_positioning:
  154. x = params.x if params.x is not None else x
  155. y = params.y if params.y is not None else y
  156. z = params.z if params.z is not None else z
  157. else:
  158. x += params.x if params.x is not None else 0
  159. y += params.y if params.y is not None else 0
  160. z += params.z if params.z is not None else 0
  161. f = params.f if params.f is not None else f
  162. if params.e is not None:
  163. new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
  164. if new_extrusion_value > e[self._extruder_number]:
  165. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
  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. 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: Position, 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: Position, 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: Position, 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: Position, 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. return self._position(
  205. params.x if params.x is not None else position.x,
  206. params.y if params.y is not None else position.y,
  207. params.z if params.z is not None else position.z,
  208. params.f if params.f is not None else position.f,
  209. position.e)
  210. def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  211. func = getattr(self, "_gCode%s" % G, None)
  212. line = line.split(";", 1)[0] # Remove comments (if any)
  213. if func is not None:
  214. s = line.upper().split(" ")
  215. x, y, z, f, e = None, None, None, None, None
  216. for item in s[1:]:
  217. if len(item) <= 1:
  218. continue
  219. if item.startswith(";"):
  220. continue
  221. if item[0] == "X":
  222. x = float(item[1:])
  223. if item[0] == "Y":
  224. y = float(item[1:])
  225. if item[0] == "Z":
  226. z = float(item[1:])
  227. if item[0] == "F":
  228. f = float(item[1:]) / 60
  229. if item[0] == "E":
  230. e = float(item[1:])
  231. params = self._position(x, y, z, f, e)
  232. return func(position, params, path)
  233. return position
  234. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  235. self._extruder_number = T
  236. if self._extruder_number + 1 > len(position.e):
  237. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  238. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  239. return position
  240. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  241. pass
  242. _type_keyword = ";TYPE:"
  243. _layer_keyword = ";LAYER:"
  244. ## For showing correct x, y offsets for each extruder
  245. def _extruderOffsets(self) -> Dict[int, List[float]]:
  246. result = {}
  247. for extruder in ExtruderManager.getInstance().getExtruderStacks():
  248. result[int(extruder.getMetaData().get("position", "0"))] = [
  249. extruder.getProperty("machine_nozzle_offset_x", "value"),
  250. extruder.getProperty("machine_nozzle_offset_y", "value")]
  251. return result
  252. def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
  253. Logger.log("d", "Preparing to load GCode")
  254. self._cancelled = False
  255. # We obtain the filament diameter from the selected extruder to calculate line widths
  256. global_stack = Application.getInstance().getGlobalContainerStack()
  257. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].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. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  265. ##############################################################################################
  266. ## This part is where the action starts
  267. ##############################################################################################
  268. file_lines = 0
  269. current_line = 0
  270. for line in stream.split("\n"):
  271. file_lines += 1
  272. gcode_list.append(line + "\n")
  273. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  274. self._is_layers_in_file = True
  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 Gcode...")
  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 stream.split("\n"):
  289. if self._cancelled:
  290. Logger.log("d", "Parsing Gcode file cancelled")
  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 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. # Start the new layer at the end position of the last layer
  321. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  322. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  323. # as in ProcessSlicedLayersJob
  324. if layer_number < min_layer_number:
  325. min_layer_number = layer_number
  326. if layer_number < 0:
  327. layer_number += abs(min_layer_number)
  328. negative_layers += 1
  329. else:
  330. layer_number += negative_layers
  331. # In case there is a gap in the layer count, empty layers are created
  332. for empty_layer in range(previous_layer + 1, layer_number):
  333. self._createEmptyLayer(empty_layer)
  334. self._layer_number = layer_number
  335. previous_layer = layer_number
  336. except:
  337. pass
  338. # This line is a comment. Ignore it (except for the layer_keyword)
  339. if line.startswith(";"):
  340. continue
  341. G = self._getInt(line, "G")
  342. if G is not None:
  343. # When find a movement, the new posistion is calculated and added to the current_path, but
  344. # don't need to create a polygon until the end of the layer
  345. current_position = self.processGCode(G, line, current_position, current_path)
  346. continue
  347. # When changing the extruder, the polygon with the stored paths is computed
  348. if line.startswith("T"):
  349. T = self._getInt(line, "T")
  350. if T is not None:
  351. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  352. current_path.clear()
  353. # When changing tool, store the end point of the previous path, then process the code and finally
  354. # add another point with the new position of the head.
  355. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  356. current_position = self.processTCode(T, line, current_position, current_path)
  357. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  358. if line.startswith("M"):
  359. M = self._getInt(line, "M")
  360. self.processMCode(M, line, current_position, current_path)
  361. # "Flush" leftovers. Last layer paths are still stored
  362. if len(current_path) > 1:
  363. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  364. self._layer_number += 1
  365. current_path.clear()
  366. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  367. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  368. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  369. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  370. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  371. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  372. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  373. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  374. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  375. layer_mesh = self._layer_data_builder.build(material_color_map)
  376. decorator = LayerDataDecorator()
  377. decorator.setLayerData(layer_mesh)
  378. scene_node.addDecorator(decorator)
  379. gcode_list_decorator = GCodeListDecorator()
  380. gcode_list_decorator.setGCodeList(gcode_list)
  381. scene_node.addDecorator(gcode_list_decorator)
  382. # gcode_dict stores gcode_lists for a number of build plates.
  383. active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  384. gcode_dict = {active_build_plate_id: gcode_list}
  385. Application.getInstance().getController().getScene().gcode_dict = gcode_dict
  386. Logger.log("d", "Finished parsing Gcode")
  387. self._message.hide()
  388. if self._layer_number == 0:
  389. Logger.log("w", "File doesn't contain any valid layers")
  390. settings = Application.getInstance().getGlobalContainerStack()
  391. if not settings.getProperty("machine_center_is_zero", "value"):
  392. machine_width = settings.getProperty("machine_width", "value")
  393. machine_depth = settings.getProperty("machine_depth", "value")
  394. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  395. Logger.log("d", "GCode loading finished")
  396. if Application.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  397. caution_message = Message(catalog.i18nc(
  398. "@info:generic",
  399. "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."),
  400. lifetime=0,
  401. title = catalog.i18nc("@info:title", "G-code Details"))
  402. caution_message.show()
  403. # The "save/print" button's state is bound to the backend state.
  404. backend = Application.getInstance().getBackend()
  405. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  406. return scene_node