PrintInformation.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 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.Qt.Duration import Duration
  7. from UM.Preferences import Preferences
  8. from UM.Settings import ContainerRegistry
  9. import cura.Settings.ExtruderManager
  10. import math
  11. import os.path
  12. import unicodedata
  13. import json
  14. from UM.i18n import i18nCatalog
  15. catalog = i18nCatalog("cura")
  16. ## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
  17. #
  18. # This class contains all the logic relating to calculation and slicing for the
  19. # time/quality slider concept. It is a rather tricky combination of event handling
  20. # and state management. The logic behind this is as follows:
  21. #
  22. # - A scene change or setting change event happens.
  23. # We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
  24. # - This triggers a new slice with the current settings - this is the "current settings pass".
  25. # - When the slice is done, we update the current print time and material amount.
  26. # - 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.
  27. # - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
  28. # - When the high quality pass is done, we update the maximum print time.
  29. #
  30. # This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
  31. # This job name is requested by the JobSpecs qml file.
  32. class PrintInformation(QObject):
  33. class SlicePass:
  34. CurrentSettings = 1
  35. LowQualitySettings = 2
  36. HighQualitySettings = 3
  37. class SliceReason:
  38. SceneChanged = 1
  39. SettingChanged = 2
  40. ActiveMachineChanged = 3
  41. Other = 4
  42. def __init__(self, parent = None):
  43. super().__init__(parent)
  44. self._current_print_time = Duration(None, self)
  45. self._material_lengths = []
  46. self._material_weights = []
  47. self._material_costs = []
  48. self._pre_sliced = False
  49. self._backend = Application.getInstance().getBackend()
  50. if self._backend:
  51. self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
  52. self._job_name = ""
  53. self._abbr_machine = ""
  54. Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
  55. Application.getInstance().fileLoaded.connect(self.setJobName)
  56. Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
  57. self._active_material_container = None
  58. Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged)
  59. self._onActiveMaterialChanged()
  60. currentPrintTimeChanged = pyqtSignal()
  61. preSlicedChanged = pyqtSignal()
  62. @pyqtProperty(bool, notify=preSlicedChanged)
  63. def preSliced(self):
  64. return self._pre_sliced
  65. def setPreSliced(self, pre_sliced):
  66. self._pre_sliced = pre_sliced
  67. self.preSlicedChanged.emit()
  68. @pyqtProperty(Duration, notify = currentPrintTimeChanged)
  69. def currentPrintTime(self):
  70. return self._current_print_time
  71. materialLengthsChanged = pyqtSignal()
  72. @pyqtProperty("QVariantList", notify = materialLengthsChanged)
  73. def materialLengths(self):
  74. return self._material_lengths
  75. materialWeightsChanged = pyqtSignal()
  76. @pyqtProperty("QVariantList", notify = materialWeightsChanged)
  77. def materialWeights(self):
  78. return self._material_weights
  79. materialCostsChanged = pyqtSignal()
  80. @pyqtProperty("QVariantList", notify = materialCostsChanged)
  81. def materialCosts(self):
  82. return self._material_costs
  83. def _onPrintDurationMessage(self, total_time, material_amounts):
  84. self._current_print_time.setDuration(total_time)
  85. self.currentPrintTimeChanged.emit()
  86. self._material_amounts = material_amounts
  87. self._calculateInformation()
  88. def _calculateInformation(self):
  89. # Material amount is sent as an amount of mm^3, so calculate length from that
  90. r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
  91. self._material_lengths = []
  92. self._material_weights = []
  93. self._material_costs = []
  94. material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
  95. extruder_stacks = list(cura.Settings.ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
  96. for index, amount in enumerate(self._material_amounts):
  97. ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
  98. # list comprehension filtering to solve this for us.
  99. material = None
  100. if extruder_stacks: # Multi extrusion machine
  101. extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
  102. density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
  103. material = extruder_stack.findContainer({"type": "material"})
  104. else: # Machine with no extruder stacks
  105. density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
  106. material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"})
  107. weight = float(amount) * float(density) / 1000
  108. cost = 0
  109. if material:
  110. material_guid = material.getMetaDataEntry("GUID")
  111. if material_guid in material_preference_values:
  112. material_values = material_preference_values[material_guid]
  113. weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
  114. cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
  115. if weight_per_spool != 0:
  116. cost = cost_per_spool * weight / weight_per_spool
  117. else:
  118. cost = 0
  119. self._material_weights.append(weight)
  120. self._material_lengths.append(round((amount / (math.pi * r ** 2)) / 1000, 2))
  121. self._material_costs.append(cost)
  122. self.materialLengthsChanged.emit()
  123. self.materialWeightsChanged.emit()
  124. self.materialCostsChanged.emit()
  125. def _onPreferencesChanged(self, preference):
  126. if preference != "cura/material_settings":
  127. return
  128. self._calculateInformation()
  129. def _onActiveMaterialChanged(self):
  130. if self._active_material_container:
  131. self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged)
  132. active_material_id = Application.getInstance().getMachineManager().activeMaterialId
  133. active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
  134. if active_material_containers:
  135. self._active_material_container = active_material_containers[0]
  136. self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
  137. def _onMaterialMetaDataChanged(self):
  138. self._calculateInformation()
  139. @pyqtSlot(str)
  140. def setJobName(self, name):
  141. # Ensure that we don't use entire path but only filename
  142. name = os.path.basename(name)
  143. # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
  144. # extension. This cuts the extension off if necessary.
  145. name = os.path.splitext(name)[0]
  146. if self._job_name != name:
  147. self._job_name = name
  148. self.jobNameChanged.emit()
  149. jobNameChanged = pyqtSignal()
  150. @pyqtProperty(str, notify = jobNameChanged)
  151. def jobName(self):
  152. return self._job_name
  153. @pyqtSlot(str, result = str)
  154. def createJobName(self, base_name):
  155. base_name = self._stripAccents(base_name)
  156. self._setAbbreviatedMachineName()
  157. if self._pre_sliced:
  158. return catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
  159. elif Preferences.getInstance().getValue("cura/jobname_prefix"):
  160. return self._abbr_machine + "_" + base_name
  161. else:
  162. return base_name
  163. ## Created an acronymn-like abbreviated machine name from the currently active machine name
  164. # Called each time the global stack is switched
  165. def _setAbbreviatedMachineName(self):
  166. global_container_stack = Application.getInstance().getGlobalContainerStack()
  167. if not global_container_stack:
  168. self._abbr_machine = ""
  169. return
  170. global_stack_name = global_container_stack.getName()
  171. split_name = global_stack_name.split(" ")
  172. abbr_machine = ""
  173. for word in split_name:
  174. if word.lower() == "ultimaker":
  175. abbr_machine += "UM"
  176. elif word.isdigit():
  177. abbr_machine += word
  178. else:
  179. abbr_machine += self._stripAccents(word.strip("()[]{}#").upper())[0]
  180. self._abbr_machine = abbr_machine
  181. ## Utility method that strips accents from characters (eg: â -> a)
  182. def _stripAccents(self, str):
  183. return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')