QualityManager.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import TYPE_CHECKING, Optional, Dict, List
  4. from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
  5. from UM.Logger import Logger
  6. from UM.Util import parseBool
  7. from UM.Settings.InstanceContainer import InstanceContainer
  8. from UM.Decorators import deprecated
  9. import cura.CuraApplication
  10. from cura.Settings.ExtruderStack import ExtruderStack
  11. from cura.Machines.ContainerTree import ContainerTree # The implementation that replaces this manager, to keep the deprecated interface working.
  12. from .QualityChangesGroup import QualityChangesGroup
  13. from .QualityGroup import QualityGroup
  14. from .QualityNode import QualityNode
  15. if TYPE_CHECKING:
  16. from UM.Settings.Interfaces import DefinitionContainerInterface
  17. from cura.Settings.GlobalStack import GlobalStack
  18. from .QualityChangesGroup import QualityChangesGroup
  19. #
  20. # Similar to MaterialManager, QualityManager maintains a number of maps and trees for quality profile lookup.
  21. # The models GUI and QML use are now only dependent on the QualityManager. That means as long as the data in
  22. # QualityManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
  23. #
  24. # For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
  25. # again. This means the update is exactly the same as initialization. There are performance concerns about this approach
  26. # but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
  27. # because it's simple.
  28. #
  29. class QualityManager(QObject):
  30. __instance = None
  31. @classmethod
  32. @deprecated("Use the ContainerTree structure instead.", since = "4.3")
  33. def getInstance(cls) -> "QualityManager":
  34. if cls.__instance is None:
  35. cls.__instance = QualityManager()
  36. return cls.__instance
  37. qualitiesUpdated = pyqtSignal()
  38. def __init__(self, parent = None) -> None:
  39. super().__init__(parent)
  40. application = cura.CuraApplication.CuraApplication.getInstance()
  41. self._material_manager = application.getMaterialManager()
  42. self._container_registry = application.getContainerRegistry()
  43. self._empty_quality_container = application.empty_quality_container
  44. self._empty_quality_changes_container = application.empty_quality_changes_container
  45. # For quality lookup
  46. self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
  47. # For quality_changes lookup
  48. self._machine_quality_type_to_quality_changes_dict = {} # type: Dict[str, QualityNode]
  49. self._default_machine_definition_id = "fdmprinter"
  50. self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
  51. self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
  52. self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
  53. def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
  54. self._onContainerChanged(container)
  55. def _onContainerChanged(self, container: InstanceContainer) -> None:
  56. container_type = container.getMetaDataEntry("type")
  57. if container_type not in ("quality", "quality_changes"):
  58. return
  59. # Returns a dict of "custom profile name" -> QualityChangesGroup
  60. def getQualityChangesGroups(self, machine: "GlobalStack") -> List[QualityChangesGroup]:
  61. variant_names = [extruder.variant.getName() for extruder in machine.extruders.values()]
  62. material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in machine.extruders.values()]
  63. extruder_enabled = [extruder.isEnabled for extruder in machine.extruders.values()]
  64. machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
  65. return machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
  66. ## Gets the quality groups for the current printer.
  67. #
  68. # Both available and unavailable quality groups will be included. Whether
  69. # a quality group is available can be known via the field
  70. # ``QualityGroup.is_available``. For more details, see QualityGroup.
  71. # \return A dictionary with quality types as keys and the quality groups
  72. # for those types as values.
  73. def getQualityGroups(self, global_stack: "GlobalStack") -> Dict[str, QualityGroup]:
  74. # Gather up the variant names and material base files for each extruder.
  75. variant_names = [extruder.variant.getName() for extruder in global_stack.extruders.values()]
  76. material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruders.values()]
  77. extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruders.values()]
  78. definition_id = global_stack.definition.getId()
  79. return ContainerTree.getInstance().machines[definition_id].getQualityGroups(variant_names, material_bases, extruder_enabled)
  80. def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
  81. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  82. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  83. # (1) the machine-specific node
  84. # (2) the generic node
  85. machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
  86. default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
  87. self._default_machine_definition_id)
  88. nodes_to_check = [machine_node, default_machine_node]
  89. # Iterate over all quality_types in the machine node
  90. quality_group_dict = dict()
  91. for node in nodes_to_check:
  92. if node and node.quality_type:
  93. quality_group = QualityGroup(node.getMetaDataEntry("name", ""), node.quality_type)
  94. quality_group.setGlobalNode(node)
  95. quality_group_dict[node.quality_type] = quality_group
  96. return quality_group_dict
  97. ## Get the quality group for the preferred quality type for a certain
  98. # global stack.
  99. #
  100. # If the preferred quality type is not available, ``None`` will be
  101. # returned.
  102. # \param machine The global stack of the machine to get the preferred
  103. # quality group for.
  104. # \return The preferred quality group, or ``None`` if that is not
  105. # available.
  106. def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
  107. machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
  108. quality_groups = self.getQualityGroups(machine)
  109. result = quality_groups.get(machine_node.preferred_quality_type)
  110. if result is not None and result.is_available:
  111. return result
  112. return None # If preferred quality type is not available, leave it up for the caller.
  113. #
  114. # Methods for GUI
  115. #
  116. #
  117. # Remove the given quality changes group.
  118. #
  119. @pyqtSlot(QObject)
  120. def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
  121. Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
  122. removed_quality_changes_ids = set()
  123. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  124. for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
  125. container_id = metadata["id"]
  126. container_registry.removeContainer(container_id)
  127. removed_quality_changes_ids.add(container_id)
  128. # Reset all machines that have activated this custom profile.
  129. for global_stack in container_registry.findContainerStacks(type = "machine"):
  130. if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
  131. global_stack.qualityChanges = self._empty_quality_changes_container
  132. for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
  133. if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
  134. extruder_stack.qualityChanges = self._empty_quality_changes_container
  135. #
  136. # Rename a set of quality changes containers. Returns the new name.
  137. #
  138. @pyqtSlot(QObject, str, result = str)
  139. def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
  140. Logger.log("i", "Renaming QualityChangesGroup[%s] to [%s]", quality_changes_group.name, new_name)
  141. if new_name == quality_changes_group.name:
  142. Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
  143. return new_name
  144. new_name = self._container_registry.uniqueName(new_name)
  145. for node in quality_changes_group.getAllNodes():
  146. container = node.container
  147. if container:
  148. container.setName(new_name)
  149. quality_changes_group.name = new_name
  150. application = cura.CuraApplication.CuraApplication.getInstance()
  151. application.getMachineManager().activeQualityChanged.emit()
  152. application.getMachineManager().activeQualityGroupChanged.emit()
  153. return new_name
  154. #
  155. # Duplicates the given quality.
  156. #
  157. @pyqtSlot(str, "QVariantMap")
  158. def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
  159. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
  160. if not global_stack:
  161. Logger.log("i", "No active global stack, cannot duplicate quality changes.")
  162. return
  163. quality_group = quality_model_item["quality_group"]
  164. quality_changes_group = quality_model_item["quality_changes_group"]
  165. if quality_changes_group is None:
  166. # create global quality changes only
  167. new_name = self._container_registry.uniqueName(quality_changes_name)
  168. new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
  169. global_stack, None)
  170. self._container_registry.addContainer(new_quality_changes)
  171. else:
  172. new_name = self._container_registry.uniqueName(quality_changes_name)
  173. for node in quality_changes_group.getAllNodes():
  174. container = node.container
  175. if not container:
  176. continue
  177. new_id = self._container_registry.uniqueName(container.getId())
  178. self._container_registry.addContainer(container.duplicate(new_id, new_name))
  179. ## Create quality changes containers from the user containers in the active stacks.
  180. #
  181. # This will go through the global and extruder stacks and create quality_changes containers from
  182. # the user containers in each stack. These then replace the quality_changes containers in the
  183. # stack and clear the user settings.
  184. @pyqtSlot(str)
  185. def createQualityChanges(self, base_name: str) -> None:
  186. machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
  187. global_stack = machine_manager.activeMachine
  188. if not global_stack:
  189. return
  190. active_quality_name = machine_manager.activeQualityOrQualityChangesName
  191. if active_quality_name == "":
  192. Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
  193. return
  194. machine_manager.blurSettings.emit()
  195. if base_name is None or base_name == "":
  196. base_name = active_quality_name
  197. unique_name = self._container_registry.uniqueName(base_name)
  198. # Go through the active stacks and create quality_changes containers from the user containers.
  199. stack_list = [global_stack] + list(global_stack.extruders.values())
  200. for stack in stack_list:
  201. user_container = stack.userChanges
  202. quality_container = stack.quality
  203. quality_changes_container = stack.qualityChanges
  204. if not quality_container or not quality_changes_container:
  205. Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
  206. continue
  207. quality_type = quality_container.getMetaDataEntry("quality_type")
  208. extruder_stack = None
  209. if isinstance(stack, ExtruderStack):
  210. extruder_stack = stack
  211. new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_stack)
  212. from cura.Settings.ContainerManager import ContainerManager
  213. ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
  214. ContainerManager.getInstance()._performMerge(new_changes, user_container)
  215. self._container_registry.addContainer(new_changes)
  216. #
  217. # Create a quality changes container with the given setup.
  218. #
  219. def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
  220. extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
  221. base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
  222. new_id = base_id + "_" + new_name
  223. new_id = new_id.lower().replace(" ", "_")
  224. new_id = self._container_registry.uniqueName(new_id)
  225. # Create a new quality_changes container for the quality.
  226. quality_changes = InstanceContainer(new_id)
  227. quality_changes.setName(new_name)
  228. quality_changes.setMetaDataEntry("type", "quality_changes")
  229. quality_changes.setMetaDataEntry("quality_type", quality_type)
  230. # If we are creating a container for an extruder, ensure we add that to the container
  231. if extruder_stack is not None:
  232. quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
  233. # If the machine specifies qualities should be filtered, ensure we match the current criteria.
  234. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  235. quality_changes.setDefinition(machine_definition_id)
  236. quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
  237. return quality_changes
  238. #
  239. # Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
  240. # machine. The rule is as follows:
  241. # 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
  242. # machine.
  243. # 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
  244. # own machine definition ID for quality search.
  245. # Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
  246. # 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
  247. # definition ID specified in "quality_definition" should be used.
  248. # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
  249. # shares the same set of qualities profiles as Ultimaker 3.
  250. #
  251. def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
  252. default_definition_id: str = "fdmprinter") -> str:
  253. machine_definition_id = default_definition_id
  254. if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
  255. # Only use the machine's own quality definition ID if this machine has machine quality.
  256. machine_definition_id = machine_definition.getMetaDataEntry("quality_definition")
  257. if machine_definition_id is None:
  258. machine_definition_id = machine_definition.getId()
  259. return machine_definition_id