PrintInformation.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
  4. from UM.FlameProfiler import pyqtSlot
  5. from UM.Application import Application
  6. from UM.Logger import Logger
  7. from UM.Qt.Duration import Duration
  8. from UM.Preferences import Preferences
  9. from UM.Settings.ContainerRegistry import ContainerRegistry
  10. from cura.Settings.ExtruderManager import ExtruderManager
  11. import math
  12. import os.path
  13. import unicodedata
  14. import json
  15. from UM.i18n import i18nCatalog
  16. catalog = i18nCatalog("cura")
  17. ## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
  18. #
  19. # This class contains all the logic relating to calculation and slicing for the
  20. # time/quality slider concept. It is a rather tricky combination of event handling
  21. # and state management. The logic behind this is as follows:
  22. #
  23. # - A scene change or setting change event happens.
  24. # We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
  25. # - This triggers a new slice with the current settings - this is the "current settings pass".
  26. # - When the slice is done, we update the current print time and material amount.
  27. # - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
  28. # - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
  29. # - When the Extra Fine pass is done, we update the maximum print time.
  30. #
  31. # This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
  32. # This job name is requested by the JobSpecs qml file.
  33. class PrintInformation(QObject):
  34. class SlicePass:
  35. CurrentSettings = 1
  36. LowQualitySettings = 2
  37. HighQualitySettings = 3
  38. class SliceReason:
  39. SceneChanged = 1
  40. SettingChanged = 2
  41. ActiveMachineChanged = 3
  42. Other = 4
  43. def __init__(self, parent = None):
  44. super().__init__(parent)
  45. self.initializeCuraMessagePrintTimeProperties()
  46. self._material_lengths = []
  47. self._material_weights = []
  48. self._material_costs = []
  49. self._material_names = []
  50. self._pre_sliced = False
  51. self._backend = Application.getInstance().getBackend()
  52. if self._backend:
  53. self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
  54. self._base_name = ""
  55. self._abbr_machine = ""
  56. self._job_name = ""
  57. self._project_name = ""
  58. Application.getInstance().globalContainerStackChanged.connect(self._updateJobName)
  59. Application.getInstance().fileLoaded.connect(self.setBaseName)
  60. Application.getInstance().workspaceLoaded.connect(self.setProjectName)
  61. Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
  62. self._active_material_container = None
  63. Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged)
  64. self._onActiveMaterialChanged()
  65. self._material_amounts = []
  66. # Crate cura message translations and using translation keys initialize empty time Duration object for total time
  67. # and time for each feature
  68. def initializeCuraMessagePrintTimeProperties(self):
  69. self._current_print_time = Duration(None, self)
  70. self._print_time_message_translations = {
  71. "inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
  72. "inset_x": catalog.i18nc("@tooltip", "Inner Walls"),
  73. "skin": catalog.i18nc("@tooltip", "Skin"),
  74. "infill": catalog.i18nc("@tooltip", "Infill"),
  75. "support_infill": catalog.i18nc("@tooltip", "Support Infill"),
  76. "support_interface": catalog.i18nc("@tooltip", "Support Interface"),
  77. "support": catalog.i18nc("@tooltip", "Support"),
  78. "skirt": catalog.i18nc("@tooltip", "Skirt"),
  79. "travel": catalog.i18nc("@tooltip", "Travel"),
  80. "retract": catalog.i18nc("@tooltip", "Retractions"),
  81. "none": catalog.i18nc("@tooltip", "Other")
  82. }
  83. self._print_time_message_values = {}
  84. # Full fill message values using keys from _print_time_message_translations
  85. for key in self._print_time_message_translations.keys():
  86. self._print_time_message_values[key] = Duration(None, self)
  87. currentPrintTimeChanged = pyqtSignal()
  88. preSlicedChanged = pyqtSignal()
  89. @pyqtProperty(bool, notify=preSlicedChanged)
  90. def preSliced(self):
  91. return self._pre_sliced
  92. def setPreSliced(self, pre_sliced):
  93. self._pre_sliced = pre_sliced
  94. self.preSlicedChanged.emit()
  95. @pyqtProperty(Duration, notify = currentPrintTimeChanged)
  96. def currentPrintTime(self):
  97. return self._current_print_time
  98. materialLengthsChanged = pyqtSignal()
  99. @pyqtProperty("QVariantList", notify = materialLengthsChanged)
  100. def materialLengths(self):
  101. return self._material_lengths
  102. materialWeightsChanged = pyqtSignal()
  103. @pyqtProperty("QVariantList", notify = materialWeightsChanged)
  104. def materialWeights(self):
  105. return self._material_weights
  106. materialCostsChanged = pyqtSignal()
  107. @pyqtProperty("QVariantList", notify = materialCostsChanged)
  108. def materialCosts(self):
  109. return self._material_costs
  110. materialNamesChanged = pyqtSignal()
  111. @pyqtProperty("QVariantList", notify = materialNamesChanged)
  112. def materialNames(self):
  113. return self._material_names
  114. def _onPrintDurationMessage(self, print_time, material_amounts):
  115. self._updateTotalPrintTimePerFeature(print_time)
  116. self.currentPrintTimeChanged.emit()
  117. self._material_amounts = material_amounts
  118. self._calculateInformation()
  119. def _updateTotalPrintTimePerFeature(self, print_time):
  120. total_estimated_time = 0
  121. for feature, time in print_time.items():
  122. if time != time: # Check for NaN. Engine can sometimes give us weird values.
  123. self._print_time_message_values.get(feature).setDuration(0)
  124. Logger.log("w", "Received NaN for print duration message")
  125. continue
  126. total_estimated_time += time
  127. self._print_time_message_values.get(feature).setDuration(time)
  128. self._current_print_time.setDuration(total_estimated_time)
  129. def _calculateInformation(self):
  130. if Application.getInstance().getGlobalContainerStack() is None:
  131. return
  132. # Material amount is sent as an amount of mm^3, so calculate length from that
  133. radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
  134. self._material_lengths = []
  135. self._material_weights = []
  136. self._material_costs = []
  137. self._material_names = []
  138. material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
  139. extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
  140. for index, amount in enumerate(self._material_amounts):
  141. ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
  142. # list comprehension filtering to solve this for us.
  143. material = None
  144. if extruder_stacks: # Multi extrusion machine
  145. extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
  146. density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
  147. material = extruder_stack.findContainer({"type": "material"})
  148. else: # Machine with no extruder stacks
  149. density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
  150. material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"})
  151. weight = float(amount) * float(density) / 1000
  152. cost = 0
  153. material_name = catalog.i18nc("@label unknown material", "Unknown")
  154. if material:
  155. material_guid = material.getMetaDataEntry("GUID")
  156. material_name = material.getName()
  157. if material_guid in material_preference_values:
  158. material_values = material_preference_values[material_guid]
  159. weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
  160. cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
  161. if weight_per_spool != 0:
  162. cost = cost_per_spool * weight / weight_per_spool
  163. else:
  164. cost = 0
  165. if radius != 0:
  166. length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
  167. else:
  168. length = 0
  169. self._material_weights.append(weight)
  170. self._material_lengths.append(length)
  171. self._material_costs.append(cost)
  172. self._material_names.append(material_name)
  173. self.materialLengthsChanged.emit()
  174. self.materialWeightsChanged.emit()
  175. self.materialCostsChanged.emit()
  176. self.materialNamesChanged.emit()
  177. def _onPreferencesChanged(self, preference):
  178. if preference != "cura/material_settings":
  179. return
  180. self._calculateInformation()
  181. def _onActiveMaterialChanged(self):
  182. if self._active_material_container:
  183. try:
  184. self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged)
  185. except TypeError: #pyQtSignal gives a TypeError when disconnecting from something that is already disconnected.
  186. pass
  187. active_material_id = Application.getInstance().getMachineManager().activeMaterialId
  188. active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
  189. if active_material_containers:
  190. self._active_material_container = active_material_containers[0]
  191. self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
  192. def _onMaterialMetaDataChanged(self, *args, **kwargs):
  193. self._calculateInformation()
  194. @pyqtSlot(str)
  195. def setJobName(self, name):
  196. self._job_name = name
  197. self.jobNameChanged.emit()
  198. @pyqtSlot(str)
  199. def setProjectName(self, name):
  200. self._project_name = name
  201. self.setJobName(name)
  202. jobNameChanged = pyqtSignal()
  203. @pyqtProperty(str, notify = jobNameChanged)
  204. def jobName(self):
  205. return self._job_name
  206. def _updateJobName(self, empty_name = False):
  207. # if the project name is set, we use the project name as the job name, so the job name should not get updated
  208. # if a model file is loaded after that.
  209. if self._project_name != "":
  210. if empty_name:
  211. self._project_name = ""
  212. return
  213. if self._base_name == "":
  214. self._job_name = ""
  215. self.jobNameChanged.emit()
  216. return
  217. base_name = self._stripAccents(self._base_name)
  218. self._setAbbreviatedMachineName()
  219. if self._pre_sliced:
  220. self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
  221. elif Preferences.getInstance().getValue("cura/jobname_prefix"):
  222. # Don't add abbreviation if it already has the exact same abbreviation.
  223. if base_name.startswith(self._abbr_machine + "_"):
  224. self._job_name = base_name
  225. else:
  226. self._job_name = self._abbr_machine + "_" + base_name
  227. else:
  228. self._job_name = base_name
  229. self.jobNameChanged.emit()
  230. @pyqtProperty(str)
  231. def baseName(self):
  232. return self._base_name
  233. @pyqtSlot(str)
  234. def setBaseName(self, base_name):
  235. # Ensure that we don't use entire path but only filename
  236. name = os.path.basename(base_name)
  237. # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
  238. # extension. This cuts the extension off if necessary.
  239. name = os.path.splitext(name)[0]
  240. # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
  241. is_empty = name == ""
  242. if is_empty or (self._base_name == "" and self._base_name != name):
  243. # remove ".curaproject" suffix from (imported) the file name
  244. if name.endswith(".curaproject"):
  245. name = name[:name.rfind(".curaproject")]
  246. self._base_name = name
  247. self._updateJobName(empty_name = is_empty)
  248. ## Created an acronymn-like abbreviated machine name from the currently active machine name
  249. # Called each time the global stack is switched
  250. def _setAbbreviatedMachineName(self):
  251. global_container_stack = Application.getInstance().getGlobalContainerStack()
  252. if not global_container_stack:
  253. self._abbr_machine = ""
  254. return
  255. global_stack_name = global_container_stack.getName()
  256. split_name = global_stack_name.split(" ")
  257. abbr_machine = ""
  258. for word in split_name:
  259. if word.lower() == "ultimaker":
  260. abbr_machine += "UM"
  261. elif word.isdigit():
  262. abbr_machine += word
  263. else:
  264. stripped_word = self._stripAccents(word.strip("()[]{}#").upper())
  265. # - use only the first character if the word is too long (> 3 characters)
  266. # - use the whole word if it's not too long (<= 3 characters)
  267. if len(stripped_word) > 3:
  268. stripped_word = stripped_word[0]
  269. abbr_machine += stripped_word
  270. self._abbr_machine = abbr_machine
  271. ## Utility method that strips accents from characters (eg: â -> a)
  272. def _stripAccents(self, str):
  273. return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
  274. @pyqtSlot(result = "QVariantMap")
  275. def getFeaturePrintTimes(self):
  276. result = {}
  277. for feature, time in self._print_time_message_values.items():
  278. if feature in self._print_time_message_translations:
  279. result[self._print_time_message_translations[feature]] = time
  280. else:
  281. result[feature] = time
  282. return result
  283. # Simulate message with zero time duration
  284. def setToZeroPrintInformation(self):
  285. temp_message = {}
  286. for key in self._print_time_message_values.keys():
  287. temp_message[key] = 0
  288. temp_material_amounts = [0]
  289. self._onPrintDurationMessage(temp_message, temp_material_amounts)