FlavorParser.py 22 KB

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