FlavorParser.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import math
  4. import re
  5. from typing import Dict, List, NamedTuple, Optional, Union, Set
  6. import numpy
  7. from UM.Backend import Backend
  8. from UM.Job import Job
  9. from UM.Logger import Logger
  10. from UM.Math.Vector import Vector
  11. from UM.Message import Message
  12. from UM.i18n import i18nCatalog
  13. from cura.CuraApplication import CuraApplication
  14. from cura.LayerDataBuilder import LayerDataBuilder
  15. from cura.LayerDataDecorator import LayerDataDecorator
  16. from cura.LayerPolygon import LayerPolygon
  17. from cura.Scene.CuraSceneNode import CuraSceneNode
  18. from cura.Scene.GCodeListDecorator import GCodeListDecorator
  19. from cura.Settings.ExtruderManager import ExtruderManager
  20. catalog = i18nCatalog("cura")
  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 # type: Optional[Message]
  30. self._layer_number = 0
  31. self._extruder_number = 0
  32. # All extruder numbers that have been seen
  33. self._extruders_seen = {0} # type: Set[int]
  34. self._clearValues()
  35. self._scene_node = None
  36. # X, Y, Z position, F feedrate and E extruder values are stored
  37. self._position = Position
  38. self._is_layers_in_file = False # Does the Gcode have the layers comment?
  39. self._extruder_offsets = {} # type: Dict[int, List[float]] # 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. self._previous_extrusion_value = 0.0 # keep track of the filament retractions
  43. CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
  44. def _clearValues(self) -> None:
  45. self._extruder_number = 0
  46. self._extrusion_length_offset = [0] # type: List[float]
  47. self._layer_type = LayerPolygon.Inset0Type
  48. self._layer_number = 0
  49. self._previous_z = 0 # type: float
  50. self._layer_data_builder = LayerDataBuilder()
  51. self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
  52. self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
  53. @staticmethod
  54. def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
  55. n = line.find(code)
  56. if n < 0:
  57. return None
  58. n += len(code)
  59. pattern = re.compile("[;\\s]")
  60. match = pattern.search(line, n)
  61. m = match.start() if match is not None else -1
  62. try:
  63. if m < 0:
  64. return line[n:]
  65. return line[n:m]
  66. except:
  67. return None
  68. def _getInt(self, line: str, code: str) -> Optional[int]:
  69. value = self._getValue(line, code)
  70. try:
  71. return int(value) # type: ignore
  72. except:
  73. return None
  74. def _getFloat(self, line: str, code: str) -> Optional[float]:
  75. value = self._getValue(line, code)
  76. try:
  77. return float(value) # type: ignore
  78. except:
  79. return None
  80. def _onHideMessage(self, message: str) -> None:
  81. if message == self._message:
  82. self._cancelled = True
  83. def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
  84. countvalid = 0
  85. for point in path:
  86. if point[5] > 0:
  87. countvalid += 1
  88. if countvalid >= 2:
  89. # we know what to do now, no need to count further
  90. continue
  91. if countvalid < 2:
  92. return False
  93. try:
  94. self._layer_data_builder.addLayer(self._layer_number)
  95. self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
  96. self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
  97. this_layer = self._layer_data_builder.getLayer(self._layer_number)
  98. if not this_layer:
  99. return False
  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: PositionOptional, 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. self._previous_extrusion_value = new_extrusion_value
  168. else:
  169. path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  170. e[self._extruder_number] = new_extrusion_value
  171. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  172. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  173. if z > self._previous_z and (z - self._previous_z < 1.5):
  174. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  175. self._previous_z = z
  176. elif self._previous_extrusion_value > e[self._extruder_number]:
  177. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
  178. else:
  179. path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  180. return self._position(x, y, z, f, e)
  181. # G0 and G1 should be handled exactly the same.
  182. _gCode1 = _gCode0
  183. ## Home the head.
  184. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  185. return self._position(
  186. params.x if params.x is not None else position.x,
  187. params.y if params.y is not None else position.y,
  188. params.z if params.z is not None else position.z,
  189. position.f,
  190. position.e)
  191. ## Set the absolute positioning
  192. def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  193. self._is_absolute_positioning = True
  194. self._is_absolute_extrusion = True
  195. return position
  196. ## Set the relative positioning
  197. def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  198. self._is_absolute_positioning = False
  199. self._is_absolute_extrusion = False
  200. return position
  201. ## Reset the current position to the values specified.
  202. # For example: G92 X10 will set the X to 10 without any physical motion.
  203. def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
  204. if params.e is not None:
  205. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  206. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  207. position.e[self._extruder_number] = params.e
  208. self._previous_extrusion_value = params.e
  209. else:
  210. self._previous_extrusion_value = 0.0
  211. return self._position(
  212. params.x if params.x is not None else position.x,
  213. params.y if params.y is not None else position.y,
  214. params.z if params.z is not None else position.z,
  215. params.f if params.f is not None else position.f,
  216. position.e)
  217. def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  218. func = getattr(self, "_gCode%s" % G, None)
  219. line = line.split(";", 1)[0] # Remove comments (if any)
  220. if func is not None:
  221. s = line.upper().split(" ")
  222. x, y, z, f, e = None, None, None, None, None
  223. for item in s[1:]:
  224. if len(item) <= 1:
  225. continue
  226. if item.startswith(";"):
  227. continue
  228. try:
  229. if item[0] == "X":
  230. x = float(item[1:])
  231. if item[0] == "Y":
  232. y = float(item[1:])
  233. if item[0] == "Z":
  234. z = float(item[1:])
  235. if item[0] == "F":
  236. f = float(item[1:]) / 60
  237. if item[0] == "E":
  238. e = float(item[1:])
  239. except ValueError: # Improperly formatted g-code: Coordinates are not floats.
  240. continue # Skip the command then.
  241. params = PositionOptional(x, y, z, f, e)
  242. return func(position, params, path)
  243. return position
  244. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  245. self._extruder_number = T
  246. if self._extruder_number + 1 > len(position.e):
  247. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  248. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  249. return position
  250. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  251. pass
  252. _type_keyword = ";TYPE:"
  253. _layer_keyword = ";LAYER:"
  254. ## For showing correct x, y offsets for each extruder
  255. def _extruderOffsets(self) -> Dict[int, List[float]]:
  256. result = {}
  257. for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
  258. result[int(extruder.getMetaData().get("position", "0"))] = [
  259. extruder.getProperty("machine_nozzle_offset_x", "value"),
  260. extruder.getProperty("machine_nozzle_offset_y", "value")]
  261. return result
  262. #
  263. # CURA-6643
  264. # This function needs the filename so it can be set to the SceneNode. Otherwise, if you load a GCode file and press
  265. # F5, that gcode SceneNode will be removed because it doesn't have a file to be reloaded from.
  266. #
  267. def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
  268. Logger.log("d", "Preparing to load GCode")
  269. self._cancelled = False
  270. # We obtain the filament diameter from the selected extruder to calculate line widths
  271. global_stack = CuraApplication.getInstance().getGlobalContainerStack()
  272. if not global_stack:
  273. return None
  274. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  275. scene_node = CuraSceneNode()
  276. gcode_list = []
  277. self._is_layers_in_file = False
  278. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  279. ##############################################################################################
  280. ## This part is where the action starts
  281. ##############################################################################################
  282. file_lines = 0
  283. current_line = 0
  284. for line in stream.split("\n"):
  285. file_lines += 1
  286. gcode_list.append(line + "\n")
  287. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  288. self._is_layers_in_file = True
  289. file_step = max(math.floor(file_lines / 100), 1)
  290. self._clearValues()
  291. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  292. lifetime=0,
  293. title = catalog.i18nc("@info:title", "G-code Details"))
  294. assert(self._message is not None) # use for typing purposes
  295. self._message.setProgress(0)
  296. self._message.show()
  297. Logger.log("d", "Parsing Gcode...")
  298. current_position = Position(0, 0, 0, 0, [0])
  299. current_path = [] #type: List[List[float]]
  300. min_layer_number = 0
  301. negative_layers = 0
  302. previous_layer = 0
  303. self._previous_extrusion_value = 0.0
  304. for line in stream.split("\n"):
  305. if self._cancelled:
  306. Logger.log("d", "Parsing Gcode file cancelled")
  307. return None
  308. current_line += 1
  309. if current_line % file_step == 0:
  310. self._message.setProgress(math.floor(current_line / file_lines * 100))
  311. Job.yieldThread()
  312. if len(line) == 0:
  313. continue
  314. if line.find(self._type_keyword) == 0:
  315. type = line[len(self._type_keyword):].strip()
  316. if type == "WALL-INNER":
  317. self._layer_type = LayerPolygon.InsetXType
  318. elif type == "WALL-OUTER":
  319. self._layer_type = LayerPolygon.Inset0Type
  320. elif type == "SKIN":
  321. self._layer_type = LayerPolygon.SkinType
  322. elif type == "SKIRT":
  323. self._layer_type = LayerPolygon.SkirtType
  324. elif type == "SUPPORT":
  325. self._layer_type = LayerPolygon.SupportType
  326. elif type == "FILL":
  327. self._layer_type = LayerPolygon.InfillType
  328. elif type == "SUPPORT-INTERFACE":
  329. self._layer_type = LayerPolygon.SupportInterfaceType
  330. elif type == "PRIME-TOWER":
  331. self._layer_type = LayerPolygon.PrimeTowerType
  332. else:
  333. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
  334. # When the layer change is reached, the polygon is computed so we have just one layer per extruder
  335. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  336. try:
  337. layer_number = int(line[len(self._layer_keyword):])
  338. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  339. current_path.clear()
  340. # Start the new layer at the end position of the last layer
  341. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  342. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  343. # as in ProcessSlicedLayersJob
  344. if layer_number < min_layer_number:
  345. min_layer_number = layer_number
  346. if layer_number < 0:
  347. layer_number += abs(min_layer_number)
  348. negative_layers += 1
  349. else:
  350. layer_number += negative_layers
  351. # In case there is a gap in the layer count, empty layers are created
  352. for empty_layer in range(previous_layer + 1, layer_number):
  353. self._createEmptyLayer(empty_layer)
  354. self._layer_number = layer_number
  355. previous_layer = layer_number
  356. except:
  357. pass
  358. # This line is a comment. Ignore it (except for the layer_keyword)
  359. if line.startswith(";"):
  360. continue
  361. G = self._getInt(line, "G")
  362. if G is not None:
  363. # When find a movement, the new posistion is calculated and added to the current_path, but
  364. # don't need to create a polygon until the end of the layer
  365. current_position = self.processGCode(G, line, current_position, current_path)
  366. continue
  367. # When changing the extruder, the polygon with the stored paths is computed
  368. if line.startswith("T"):
  369. T = self._getInt(line, "T")
  370. if T is not None:
  371. self._extruders_seen.add(T)
  372. self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  373. current_path.clear()
  374. # When changing tool, store the end point of the previous path, then process the code and finally
  375. # add another point with the new position of the head.
  376. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  377. current_position = self.processTCode(T, line, current_position, current_path)
  378. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  379. if line.startswith("M"):
  380. M = self._getInt(line, "M")
  381. if M is not None:
  382. self.processMCode(M, line, current_position, current_path)
  383. # "Flush" leftovers. Last layer paths are still stored
  384. if len(current_path) > 1:
  385. if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  386. self._layer_number += 1
  387. current_path.clear()
  388. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  389. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  390. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  391. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  392. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  393. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  394. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  395. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  396. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  397. layer_mesh = self._layer_data_builder.build(material_color_map)
  398. decorator = LayerDataDecorator()
  399. decorator.setLayerData(layer_mesh)
  400. scene_node.addDecorator(decorator)
  401. gcode_list_decorator = GCodeListDecorator()
  402. gcode_list_decorator.setGcodeFileName(filename)
  403. gcode_list_decorator.setGCodeList(gcode_list)
  404. scene_node.addDecorator(gcode_list_decorator)
  405. # gcode_dict stores gcode_lists for a number of build plates.
  406. active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  407. gcode_dict = {active_build_plate_id: gcode_list}
  408. CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
  409. Logger.log("d", "Finished parsing Gcode")
  410. self._message.hide()
  411. if self._layer_number == 0:
  412. Logger.log("w", "File doesn't contain any valid layers")
  413. if not global_stack.getProperty("machine_center_is_zero", "value"):
  414. machine_width = global_stack.getProperty("machine_width", "value")
  415. machine_depth = global_stack.getProperty("machine_depth", "value")
  416. scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  417. Logger.log("d", "GCode loading finished")
  418. if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  419. caution_message = Message(catalog.i18nc(
  420. "@info:generic",
  421. "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."),
  422. lifetime=0,
  423. title = catalog.i18nc("@info:title", "G-code Details"))
  424. caution_message.show()
  425. # The "save/print" button's state is bound to the backend state.
  426. backend = CuraApplication.getInstance().getBackend()
  427. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  428. return scene_node