IntentManager.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
  4. from typing import Any, Dict, List, Set, Tuple, TYPE_CHECKING
  5. from UM.Logger import Logger
  6. from UM.Settings.InstanceContainer import InstanceContainer
  7. import cura.CuraApplication
  8. from UM.Signal import Signal
  9. from cura.Machines.ContainerTree import ContainerTree
  10. from cura.Settings.cura_empty_instance_containers import empty_intent_container
  11. if TYPE_CHECKING:
  12. from UM.Settings.InstanceContainer import InstanceContainer
  13. class IntentManager(QObject):
  14. """Front-end for querying which intents are available for a certain configuration.
  15. """
  16. __instance = None
  17. @classmethod
  18. def getInstance(cls):
  19. """This class is a singleton."""
  20. if not cls.__instance:
  21. cls.__instance = IntentManager()
  22. return cls.__instance
  23. intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
  24. intentCategoryChangedSignal = Signal()
  25. def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
  26. """Gets the metadata dictionaries of all intent profiles for a given
  27. configuration.
  28. :param definition_id: ID of the printer.
  29. :param nozzle_name: Name of the nozzle.
  30. :param material_base_file: The base_file of the material.
  31. :return: A list of metadata dictionaries matching the search criteria, or
  32. an empty list if nothing was found.
  33. """
  34. intent_metadatas = [] # type: List[Dict[str, Any]]
  35. try:
  36. materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
  37. except KeyError:
  38. Logger.log("w", "Unable to find the machine %s or the variant %s", definition_id, nozzle_name)
  39. materials = {}
  40. if material_base_file not in materials:
  41. return intent_metadatas
  42. material_node = materials[material_base_file]
  43. for quality_node in material_node.qualities.values():
  44. for intent_node in quality_node.intents.values():
  45. intent_metadatas.append(intent_node.getMetadata())
  46. return intent_metadatas
  47. def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
  48. """Collects and returns all intent categories available for the given
  49. parameters. Note that the 'default' category is always available.
  50. :param definition_id: ID of the printer.
  51. :param nozzle_name: Name of the nozzle.
  52. :param material_id: ID of the material.
  53. :return: A set of intent category names.
  54. """
  55. categories = set()
  56. for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
  57. categories.add(intent["intent_category"])
  58. categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
  59. return list(categories)
  60. def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
  61. """List of intents to be displayed in the interface.
  62. For the interface this will have to be broken up into the different
  63. intent categories. That is up to the model there.
  64. :return: A list of tuples of intent_category and quality_type. The actual
  65. instance may vary per extruder.
  66. """
  67. application = cura.CuraApplication.CuraApplication.getInstance()
  68. global_stack = application.getGlobalContainerStack()
  69. if global_stack is None:
  70. return [("default", "normal")]
  71. # TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
  72. # even though there should always be defaults. The problem then is what to do with the quality_types.
  73. # Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default.
  74. quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
  75. available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}
  76. final_intent_ids = set() # type: Set[str]
  77. current_definition_id = global_stack.definition.getId()
  78. for extruder_stack in global_stack.extruderList:
  79. if not extruder_stack.isEnabled:
  80. continue
  81. nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
  82. material_id = extruder_stack.material.getMetaDataEntry("base_file")
  83. final_intent_ids |= {metadata["id"] for metadata in self.intentMetadatas(current_definition_id, nozzle_name, material_id) if metadata.get("quality_type") in available_quality_types}
  84. result = set() # type: Set[Tuple[str, str]]
  85. for intent_id in final_intent_ids:
  86. intent_metadata = application.getContainerRegistry().findContainersMetadata(id = intent_id)[0]
  87. result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
  88. return list(result)
  89. def currentAvailableIntentCategories(self) -> List[str]:
  90. """List of intent categories available in either of the extruders.
  91. This is purposefully inconsistent with the way that the quality types
  92. are listed. The quality types will show all quality types available in
  93. the printer using any configuration. This will only list the intent
  94. categories that are available using the current configuration (but the
  95. union over the extruders).
  96. :return: List of all categories in the current configurations of all
  97. extruders.
  98. """
  99. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
  100. if global_stack is None:
  101. return ["default"]
  102. current_definition_id = global_stack.definition.getId()
  103. final_intent_categories = set() # type: Set[str]
  104. for extruder_stack in global_stack.extruderList:
  105. if not extruder_stack.isEnabled:
  106. continue
  107. nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
  108. material_id = extruder_stack.material.getMetaDataEntry("base_file")
  109. final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
  110. return list(final_intent_categories)
  111. def getDefaultIntent(self) -> "InstanceContainer":
  112. """The intent that gets selected by default when no intent is available for
  113. the configuration, an extruder can't match the intent that the user
  114. selects, or just when creating a new printer.
  115. """
  116. return empty_intent_container
  117. @pyqtProperty(str, notify = intentCategoryChanged)
  118. def currentIntentCategory(self) -> str:
  119. application = cura.CuraApplication.CuraApplication.getInstance()
  120. global_stack = application.getGlobalContainerStack()
  121. active_intent = "default"
  122. if global_stack is None:
  123. return active_intent
  124. # Loop over all active extruders and check if they have an intent that isn't default.
  125. # The logic behind this is that support materials (for instance, PVA) don't have intents, but they should be
  126. # combinable with all other intents. So if one extruder has "engineering" as an intent and the other has
  127. # "default" the 'dominant' intent is "engineering"
  128. for extruder_stack in global_stack.extruderList:
  129. if not extruder_stack.isEnabled: # Ignore disabled stacks
  130. continue
  131. extruder_intent = extruder_stack.intent.getMetaDataEntry("intent_category", "")
  132. if extruder_intent != "default":
  133. active_intent = extruder_intent
  134. return active_intent
  135. @pyqtSlot(str, str)
  136. def selectIntent(self, intent_category: str, quality_type: str) -> None:
  137. """Apply intent on the stacks."""
  138. Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
  139. old_intent_category = self.currentIntentCategory
  140. application = cura.CuraApplication.CuraApplication.getInstance()
  141. global_stack = application.getGlobalContainerStack()
  142. if global_stack is None:
  143. return
  144. current_definition_id = global_stack.definition.getId()
  145. machine_node = ContainerTree.getInstance().machines[current_definition_id]
  146. for extruder_stack in global_stack.extruderList:
  147. nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
  148. material_id = extruder_stack.material.getMetaDataEntry("base_file")
  149. material_node = machine_node.variants[nozzle_name].materials[material_id]
  150. # Since we want to switch to a certain quality type, check the tree if we have one.
  151. quality_node = None
  152. for q_node in material_node.qualities.values():
  153. if q_node.quality_type == quality_type:
  154. quality_node = q_node
  155. if quality_node is None:
  156. Logger.log("w", "Unable to find quality_type [%s] for extruder [%s]", quality_type, extruder_stack.getId())
  157. continue
  158. # Check that quality node if we can find a matching intent.
  159. intent_id = None
  160. for id, intent_node in quality_node.intents.items():
  161. if intent_node.intent_category == intent_category:
  162. intent_id = id
  163. intent = application.getContainerRegistry().findContainers(id = intent_id)
  164. if intent:
  165. extruder_stack.intent = intent[0]
  166. else:
  167. extruder_stack.intent = self.getDefaultIntent()
  168. application.getMachineManager().setQualityGroupByQualityType(quality_type)
  169. if old_intent_category != intent_category:
  170. self.intentCategoryChanged.emit()
  171. self.intentCategoryChangedSignal.emit()