QualityManager.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import TYPE_CHECKING, Optional
  4. from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
  5. from UM.Application import Application
  6. from UM.Logger import Logger
  7. from UM.Util import parseBool
  8. from UM.Settings.InstanceContainer import InstanceContainer
  9. from cura.Settings.ExtruderStack import ExtruderStack
  10. from .QualityGroup import QualityGroup
  11. from .QualityNode import QualityNode
  12. if TYPE_CHECKING:
  13. from UM.Settings.DefinitionContainer import DefinitionContainer
  14. from cura.Settings.GlobalStack import GlobalStack
  15. from .QualityChangesGroup import QualityChangesGroup
  16. #
  17. # Similar to MaterialManager, QualityManager maintains a number of maps and trees for quality profile lookup.
  18. # The models GUI and QML use are now only dependent on the QualityManager. That means as long as the data in
  19. # QualityManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
  20. #
  21. # For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
  22. # again. This means the update is exactly the same as initialization. There are performance concerns about this approach
  23. # but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
  24. # because it's simple.
  25. #
  26. class QualityManager(QObject):
  27. qualitiesUpdated = pyqtSignal()
  28. def __init__(self, container_registry, parent = None):
  29. super().__init__(parent)
  30. self._application = Application.getInstance()
  31. self._material_manager = self._application.getMaterialManager()
  32. self._container_registry = container_registry
  33. self._empty_quality_container = self._application.empty_quality_container
  34. self._empty_quality_changes_container = self._application.empty_quality_changes_container
  35. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  36. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  37. self._default_machine_definition_id = "fdmprinter"
  38. self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
  39. self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
  40. self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
  41. # When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
  42. # we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
  43. # we don't react too many time.
  44. self._update_timer = QTimer(self)
  45. self._update_timer.setInterval(300)
  46. self._update_timer.setSingleShot(True)
  47. self._update_timer.timeout.connect(self._updateMaps)
  48. def initialize(self):
  49. # Initialize the lookup tree for quality profiles with following structure:
  50. # <machine> -> <variant> -> <material>
  51. # -> <material>
  52. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  53. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  54. quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
  55. for metadata in quality_metadata_list:
  56. if metadata["id"] == "empty_quality":
  57. continue
  58. definition_id = metadata["definition"]
  59. quality_type = metadata["quality_type"]
  60. root_material_id = metadata.get("material")
  61. variant_name = metadata.get("variant")
  62. is_global_quality = metadata.get("global_quality", False)
  63. is_global_quality = is_global_quality or (root_material_id is None and variant_name is None)
  64. # Sanity check: material+variant and is_global_quality cannot be present at the same time
  65. if is_global_quality and (root_material_id or variant_name):
  66. raise RuntimeError("Quality profile [%s] contains invalid data: it is a global quality but contains 'material' and 'nozzle' info." % metadata["id"])
  67. if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
  68. self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
  69. machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id]
  70. if is_global_quality:
  71. # For global qualities, save data in the machine node
  72. machine_node.addQualityMetadata(quality_type, metadata)
  73. continue
  74. if variant_name is not None:
  75. # If variant_name is specified in the quality/quality_changes profile, check if material is specified,
  76. # too.
  77. if variant_name not in machine_node.children_map:
  78. machine_node.children_map[variant_name] = QualityNode()
  79. variant_node = machine_node.children_map[variant_name]
  80. if root_material_id is None:
  81. # If only variant_name is specified but material is not, add the quality/quality_changes metadata
  82. # into the current variant node.
  83. variant_node.addQualityMetadata(quality_type, metadata)
  84. else:
  85. # If only variant_name and material are both specified, go one level deeper: create a material node
  86. # under the current variant node, and then add the quality/quality_changes metadata into the
  87. # material node.
  88. if root_material_id not in variant_node.children_map:
  89. variant_node.children_map[root_material_id] = QualityNode()
  90. material_node = variant_node.children_map[root_material_id]
  91. material_node.addQualityMetadata(quality_type, metadata)
  92. else:
  93. # If variant_name is not specified, check if material is specified.
  94. if root_material_id is not None:
  95. if root_material_id not in machine_node.children_map:
  96. machine_node.children_map[root_material_id] = QualityNode()
  97. material_node = machine_node.children_map[root_material_id]
  98. material_node.addQualityMetadata(quality_type, metadata)
  99. # Initialize the lookup tree for quality_changes profiles with following structure:
  100. # <machine> -> <quality_type> -> <name>
  101. quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
  102. for metadata in quality_changes_metadata_list:
  103. if metadata["id"] == "empty_quality_changes":
  104. continue
  105. machine_definition_id = metadata["definition"]
  106. quality_type = metadata["quality_type"]
  107. if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
  108. self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
  109. machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
  110. machine_node.addQualityChangesMetadata(quality_type, metadata)
  111. Logger.log("d", "Lookup tables updated.")
  112. self.qualitiesUpdated.emit()
  113. def _updateMaps(self):
  114. self.initialize()
  115. def _onContainerMetadataChanged(self, container):
  116. self._onContainerChanged(container)
  117. def _onContainerChanged(self, container):
  118. container_type = container.getMetaDataEntry("type")
  119. if container_type not in ("quality", "quality_changes"):
  120. return
  121. # update the cache table
  122. self._update_timer.start()
  123. # Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
  124. def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
  125. used_extruders = set()
  126. for i in range(machine.getProperty("machine_extruder_count", "value")):
  127. if machine.extruders[str(i)].isEnabled:
  128. used_extruders.add(str(i))
  129. # Update the "is_available" flag for each quality group.
  130. for quality_group in quality_group_list:
  131. is_available = True
  132. if quality_group.node_for_global is None:
  133. is_available = False
  134. if is_available:
  135. for position in used_extruders:
  136. if position not in quality_group.nodes_for_extruders:
  137. is_available = False
  138. break
  139. quality_group.is_available = is_available
  140. # Returns a dict of "custom profile name" -> QualityChangesGroup
  141. def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
  142. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  143. machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
  144. if not machine_node:
  145. Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
  146. return dict()
  147. # Update availability for each QualityChangesGroup:
  148. # A custom profile is always available as long as the quality_type it's based on is available
  149. quality_group_dict = self.getQualityGroups(machine)
  150. available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
  151. # Iterate over all quality_types in the machine node
  152. quality_changes_group_dict = dict()
  153. for quality_type, quality_changes_node in machine_node.quality_type_map.items():
  154. for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
  155. quality_changes_group_dict[quality_changes_name] = quality_changes_group
  156. quality_changes_group.is_available = quality_type in available_quality_type_list
  157. return quality_changes_group_dict
  158. #
  159. # Gets all quality groups for the given machine. Both available and none available ones will be included.
  160. # It returns a dictionary with "quality_type"s as keys and "QualityGroup"s as values.
  161. # Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
  162. # For more details, see QualityGroup.
  163. #
  164. def getQualityGroups(self, machine: "GlobalStack") -> dict:
  165. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  166. # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
  167. has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
  168. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  169. # (1) the machine-specific node
  170. # (2) the generic node
  171. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  172. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
  173. nodes_to_check = [machine_node, default_machine_node]
  174. # Iterate over all quality_types in the machine node
  175. quality_group_dict = {}
  176. for node in nodes_to_check:
  177. if node and node.quality_type_map:
  178. # Only include global qualities
  179. if has_variant_materials:
  180. quality_node = list(node.quality_type_map.values())[0]
  181. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  182. if not is_global_quality:
  183. continue
  184. for quality_type, quality_node in node.quality_type_map.items():
  185. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  186. quality_group.node_for_global = quality_node
  187. quality_group_dict[quality_type] = quality_group
  188. break
  189. # Iterate over all extruders to find quality containers for each extruder
  190. for position, extruder in machine.extruders.items():
  191. variant_name = None
  192. if extruder.variant.getId() != "empty_variant":
  193. variant_name = extruder.variant.getName()
  194. # This is a list of root material IDs to use for searching for suitable quality profiles.
  195. # The root material IDs in this list are in prioritized order.
  196. root_material_id_list = []
  197. has_material = False # flag indicating whether this extruder has a material assigned
  198. if extruder.material.getId() != "empty_material":
  199. has_material = True
  200. root_material_id = extruder.material.getMetaDataEntry("base_file")
  201. # Convert possible generic_pla_175 -> generic_pla
  202. root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
  203. root_material_id_list.append(root_material_id)
  204. # Also try to get the fallback material
  205. material_type = extruder.material.getMetaDataEntry("material")
  206. fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
  207. if fallback_root_material_id:
  208. root_material_id_list.append(fallback_root_material_id)
  209. # Here we construct a list of nodes we want to look for qualities with the highest priority first.
  210. # The use case is that, when we look for qualities for a machine, we first want to search in the following
  211. # order:
  212. # 1. machine-variant-and-material-specific qualities if exist
  213. # 2. machine-variant-specific qualities if exist
  214. # 3. machine-material-specific qualities if exist
  215. # 4. machine-specific qualities if exist
  216. # 5. generic qualities if exist
  217. # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
  218. # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
  219. # qualities from there.
  220. nodes_to_check = []
  221. if variant_name:
  222. # In this case, we have both a specific variant and a specific material
  223. variant_node = machine_node.getChildNode(variant_name)
  224. if variant_node and has_material:
  225. for root_material_id in root_material_id_list:
  226. material_node = variant_node.getChildNode(root_material_id)
  227. if material_node:
  228. nodes_to_check.append(material_node)
  229. break
  230. nodes_to_check.append(variant_node)
  231. # In this case, we only have a specific material but NOT a variant
  232. if has_material:
  233. for root_material_id in root_material_id_list:
  234. material_node = machine_node.getChildNode(root_material_id)
  235. if material_node:
  236. nodes_to_check.append(material_node)
  237. break
  238. nodes_to_check += [machine_node, default_machine_node]
  239. for node in nodes_to_check:
  240. if node and node.quality_type_map:
  241. if has_variant_materials:
  242. # Only include variant qualities; skip non global qualities
  243. quality_node = list(node.quality_type_map.values())[0]
  244. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  245. if is_global_quality:
  246. continue
  247. for quality_type, quality_node in node.quality_type_map.items():
  248. if quality_type not in quality_group_dict:
  249. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  250. quality_group_dict[quality_type] = quality_group
  251. quality_group = quality_group_dict[quality_type]
  252. quality_group.nodes_for_extruders[position] = quality_node
  253. break
  254. # Update availabilities for each quality group
  255. self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
  256. return quality_group_dict
  257. def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
  258. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  259. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  260. # (1) the machine-specific node
  261. # (2) the generic node
  262. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  263. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
  264. self._default_machine_definition_id)
  265. nodes_to_check = [machine_node, default_machine_node]
  266. # Iterate over all quality_types in the machine node
  267. quality_group_dict = dict()
  268. for node in nodes_to_check:
  269. if node and node.quality_type_map:
  270. for quality_type, quality_node in node.quality_type_map.items():
  271. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  272. quality_group.node_for_global = quality_node
  273. quality_group_dict[quality_type] = quality_group
  274. break
  275. return quality_group_dict
  276. #
  277. # Methods for GUI
  278. #
  279. #
  280. # Remove the given quality changes group.
  281. #
  282. @pyqtSlot(QObject)
  283. def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"):
  284. Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
  285. for node in quality_changes_group.getAllNodes():
  286. self._container_registry.removeContainer(node.metadata["id"])
  287. #
  288. # Rename a set of quality changes containers. Returns the new name.
  289. #
  290. @pyqtSlot(QObject, str, result = str)
  291. def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
  292. Logger.log("i", "Renaming QualityChangesGroup[%s] to [%s]", quality_changes_group.name, new_name)
  293. if new_name == quality_changes_group.name:
  294. Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
  295. return new_name
  296. new_name = self._container_registry.uniqueName(new_name)
  297. for node in quality_changes_group.getAllNodes():
  298. node.getContainer().setName(new_name)
  299. quality_changes_group.name = new_name
  300. self._application.getMachineManager().activeQualityChanged.emit()
  301. self._application.getMachineManager().activeQualityGroupChanged.emit()
  302. return new_name
  303. #
  304. # Duplicates the given quality.
  305. #
  306. @pyqtSlot(str, "QVariantMap")
  307. def duplicateQualityChanges(self, quality_changes_name, quality_model_item):
  308. global_stack = self._application.getGlobalContainerStack()
  309. if not global_stack:
  310. Logger.log("i", "No active global stack, cannot duplicate quality changes.")
  311. return
  312. quality_group = quality_model_item["quality_group"]
  313. quality_changes_group = quality_model_item["quality_changes_group"]
  314. if quality_changes_group is None:
  315. # create global quality changes only
  316. new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
  317. global_stack, None)
  318. self._container_registry.addContainer(new_quality_changes)
  319. else:
  320. new_name = self._container_registry.uniqueName(quality_changes_name)
  321. for node in quality_changes_group.getAllNodes():
  322. container = node.getContainer()
  323. new_id = self._container_registry.uniqueName(container.getId())
  324. self._container_registry.addContainer(container.duplicate(new_id, new_name))
  325. ## Create quality changes containers from the user containers in the active stacks.
  326. #
  327. # This will go through the global and extruder stacks and create quality_changes containers from
  328. # the user containers in each stack. These then replace the quality_changes containers in the
  329. # stack and clear the user settings.
  330. @pyqtSlot(str)
  331. def createQualityChanges(self, base_name):
  332. machine_manager = Application.getInstance().getMachineManager()
  333. global_stack = machine_manager.activeMachine
  334. if not global_stack:
  335. return
  336. active_quality_name = machine_manager.activeQualityOrQualityChangesName
  337. if active_quality_name == "":
  338. Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
  339. return
  340. machine_manager.blurSettings.emit()
  341. if base_name is None or base_name == "":
  342. base_name = active_quality_name
  343. unique_name = self._container_registry.uniqueName(base_name)
  344. # Go through the active stacks and create quality_changes containers from the user containers.
  345. stack_list = [global_stack] + list(global_stack.extruders.values())
  346. for stack in stack_list:
  347. user_container = stack.userChanges
  348. quality_container = stack.quality
  349. quality_changes_container = stack.qualityChanges
  350. if not quality_container or not quality_changes_container:
  351. Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
  352. continue
  353. quality_type = quality_container.getMetaDataEntry("quality_type")
  354. extruder_stack = None
  355. if isinstance(stack, ExtruderStack):
  356. extruder_stack = stack
  357. new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_stack)
  358. from cura.Settings.ContainerManager import ContainerManager
  359. ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
  360. ContainerManager.getInstance()._performMerge(new_changes, user_container)
  361. self._container_registry.addContainer(new_changes)
  362. #
  363. # Create a quality changes container with the given setup.
  364. #
  365. def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
  366. extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
  367. base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
  368. new_id = base_id + "_" + new_name
  369. new_id = new_id.lower().replace(" ", "_")
  370. new_id = self._container_registry.uniqueName(new_id)
  371. # Create a new quality_changes container for the quality.
  372. quality_changes = InstanceContainer(new_id)
  373. quality_changes.setName(new_name)
  374. quality_changes.addMetaDataEntry("type", "quality_changes")
  375. quality_changes.addMetaDataEntry("quality_type", quality_type)
  376. # If we are creating a container for an extruder, ensure we add that to the container
  377. if extruder_stack is not None:
  378. quality_changes.addMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
  379. # If the machine specifies qualities should be filtered, ensure we match the current criteria.
  380. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
  381. quality_changes.setDefinition(machine_definition_id)
  382. quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion)
  383. return quality_changes
  384. #
  385. # Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
  386. # machine. The rule is as follows:
  387. # 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
  388. # machine.
  389. # 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
  390. # own machine definition ID for quality search.
  391. # Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
  392. # 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
  393. # definition ID specified in "quality_definition" should be used.
  394. # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
  395. # shares the same set of qualities profiles as Ultimaker 3.
  396. #
  397. def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
  398. default_definition_id: str = "fdmprinter") -> str:
  399. machine_definition_id = default_definition_id
  400. if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
  401. # Only use the machine's own quality definition ID if this machine has machine quality.
  402. machine_definition_id = machine_definition.getMetaDataEntry("quality_definition")
  403. if machine_definition_id is None:
  404. machine_definition_id = machine_definition.getId()
  405. return machine_definition_id