QualityManager.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Optional, List
  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 cura.Machines.ContainerNode import ContainerNode
  9. #
  10. # Quality lookup tree structure:
  11. #
  12. # <machine_definition_id>------|
  13. # | |
  14. # <variant_name> <root_material_id>
  15. # |
  16. # <root_material_id>
  17. # |
  18. # <quality_type>
  19. # |
  20. # <quality_name>
  21. # + <quality_changes_name>
  22. #
  23. #
  24. # A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
  25. # Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
  26. # must be applied to all stacks in a machine, although each stack can have different containers. Use an Ultimaker 3
  27. # as an example, suppose we choose quality type "normal", the actual InstanceContainers on each stack may look
  28. # as below:
  29. # GlobalStack ExtruderStack 1 ExtruderStack 2
  30. # quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
  31. #
  32. # This QualityGroup is mainly used in quality and quality_changes to group the containers that can be applied to
  33. # a machine, so when a quality/custom quality is selected, the container can be directly applied to each stack instead
  34. # of looking them up again.
  35. #
  36. class QualityGroup(QObject):
  37. def __init__(self, name: str, quality_type: str, parent = None):
  38. super().__init__(parent)
  39. self.name = name
  40. self.node_for_global = None # type: Optional["QualityGroup"]
  41. self.nodes_for_extruders = dict() # position str -> QualityGroup
  42. self.quality_type = quality_type
  43. self.is_available = False
  44. @pyqtSlot(result = str)
  45. def getName(self) -> str:
  46. return self.name
  47. def getAllKeys(self) -> set:
  48. result = set()
  49. for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
  50. if node is None:
  51. continue
  52. for key in node.getContainer().getAllKeys():
  53. result.add(key)
  54. return result
  55. def getAllNodes(self) -> List["QualityGroup"]:
  56. result = []
  57. if self.node_for_global is not None:
  58. result.append(self.node_for_global)
  59. for extruder_node in self.nodes_for_extruders.values():
  60. result.append(extruder_node)
  61. return result
  62. class QualityChangesGroup(QualityGroup):
  63. def __init__(self, name: str, quality_type: str, parent = None):
  64. super().__init__(name, quality_type, parent)
  65. def addNode(self, node: "QualityNode"):
  66. # TODO: in 3.2 and earlier, a quality_changes container may have a field called "extruder" which contains the
  67. # extruder definition ID it belongs to. But, in fact, we only need to know the following things:
  68. # 1. which machine a custom profile is suitable for,
  69. # 2. if this profile is for the GlobalStack,
  70. # 3. if this profile is for an ExtruderStack and which one (the position).
  71. #
  72. # So, it is preferred to have a field like this:
  73. # extruder_position = 1
  74. # instead of this:
  75. # extruder = custom_extruder_1
  76. #
  77. # An upgrade needs to be done if we want to do it this way. Before that, we use the extruder's definition
  78. # to figure out its position.
  79. #
  80. extruder_definition_id = node.metadata.get("extruder")
  81. if extruder_definition_id:
  82. container_registry = Application.getInstance().getContainerRegistry()
  83. metadata_list = container_registry.findDefinitionContainersMetadata(id = extruder_definition_id)
  84. if not metadata_list:
  85. raise RuntimeError("%s cannot get metadata for extruder definition [%s]" %
  86. (self, extruder_definition_id))
  87. extruder_definition_metadata = metadata_list[0]
  88. extruder_position = str(extruder_definition_metadata["position"])
  89. if extruder_position in self.nodes_for_extruders:
  90. raise RuntimeError("%s tries to overwrite the existing nodes_for_extruders position [%s] %s with %s" %
  91. (self, extruder_position, self.node_for_global, node))
  92. self.nodes_for_extruders[extruder_position] = node
  93. else:
  94. # This is a quality_changes for the GlobalStack
  95. if self.node_for_global is not None:
  96. raise RuntimeError("%s tries to overwrite the existing node_for_global %s with %s" %
  97. (self, self.node_for_global, node))
  98. self.node_for_global = node
  99. def __str__(self) -> str:
  100. return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)
  101. #
  102. # QualityNode is used for BOTH quality and quality_changes containers.
  103. #
  104. class QualityNode(ContainerNode):
  105. def __init__(self, metadata: Optional[dict] = None):
  106. super().__init__(metadata = metadata)
  107. self.quality_type_map = {} # quality_type -> QualityNode for InstanceContainer
  108. def addQualityMetadata(self, quality_type: str, metadata: dict):
  109. if quality_type not in self.quality_type_map:
  110. self.quality_type_map[quality_type] = QualityNode(metadata)
  111. def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
  112. return self.quality_type_map.get(quality_type)
  113. def addQualityChangesMetadata(self, quality_type: str, metadata: dict):
  114. if quality_type not in self.quality_type_map:
  115. self.quality_type_map[quality_type] = QualityNode()
  116. quality_type_node = self.quality_type_map[quality_type]
  117. name = metadata["name"]
  118. if name not in quality_type_node.children_map:
  119. quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
  120. quality_changes_group = quality_type_node.children_map[name]
  121. quality_changes_group.addNode(QualityNode(metadata))
  122. class QualityManager(QObject):
  123. qualitiesUpdated = pyqtSignal()
  124. def __init__(self, container_registry, parent = None):
  125. super().__init__(parent)
  126. self._application = Application.getInstance()
  127. self._material_manager = self._application._material_manager
  128. self._container_registry = container_registry
  129. self._empty_quality_container = self._application.empty_quality_container
  130. self._empty_quality_changes_container = self._application.empty_quality_changes_container
  131. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  132. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  133. self._default_machine_definition_id = "fdmprinter"
  134. self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
  135. self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
  136. self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
  137. # When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
  138. # we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
  139. # we don't react too many time.
  140. self._update_timer = QTimer(self)
  141. self._update_timer.setInterval(300)
  142. self._update_timer.setSingleShot(True)
  143. self._update_timer.timeout.connect(self._updateMaps)
  144. def initialize(self):
  145. # Initialize the lookup tree for quality profiles with following structure:
  146. # <machine> -> <variant> -> <material>
  147. # -> <material>
  148. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  149. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  150. quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
  151. for metadata in quality_metadata_list:
  152. if metadata["id"] == "empty_quality":
  153. continue
  154. definition_id = metadata["definition"]
  155. quality_type = metadata["quality_type"]
  156. root_material_id = metadata.get("material")
  157. variant_name = metadata.get("variant")
  158. is_global_quality = metadata.get("global_quality", False)
  159. is_global_quality = is_global_quality or (root_material_id is None and variant_name is None)
  160. # Sanity check: material+variant and is_global_quality cannot be present at the same time
  161. if is_global_quality and (root_material_id or variant_name):
  162. raise RuntimeError("Quality profile [%s] contains invalid data: it is a global quality but contains 'material' and 'nozzle' info." % metadata["id"])
  163. if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
  164. self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
  165. machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id]
  166. if is_global_quality:
  167. # For global qualities, save data in the machine node
  168. machine_node.addQualityMetadata(quality_type, metadata)
  169. continue
  170. if variant_name is not None:
  171. # If variant_name is specified in the quality/quality_changes profile, check if material is specified,
  172. # too.
  173. if variant_name not in machine_node.children_map:
  174. machine_node.children_map[variant_name] = QualityNode()
  175. variant_node = machine_node.children_map[variant_name]
  176. if root_material_id is None:
  177. # If only variant_name is specified but material is not, add the quality/quality_changes metadata
  178. # into the current variant node.
  179. variant_node.addQualityMetadata(quality_type, metadata)
  180. else:
  181. # If only variant_name and material are both specified, go one level deeper: create a material node
  182. # under the current variant node, and then add the quality/quality_changes metadata into the
  183. # material node.
  184. if root_material_id not in variant_node.children_map:
  185. variant_node.children_map[root_material_id] = QualityNode()
  186. material_node = variant_node.children_map[root_material_id]
  187. material_node.addQualityMetadata(quality_type, metadata)
  188. else:
  189. # If variant_name is not specified, check if material is specified.
  190. if root_material_id is not None:
  191. if root_material_id not in machine_node.children_map:
  192. machine_node.children_map[root_material_id] = QualityNode()
  193. material_node = machine_node.children_map[root_material_id]
  194. material_node.addQualityMetadata(quality_type, metadata)
  195. # Initialize the lookup tree for quality_changes profiles with following structure:
  196. # <machine> -> <quality_type> -> <name>
  197. quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
  198. for metadata in quality_changes_metadata_list:
  199. if metadata["id"] == "empty_quality_changes":
  200. continue
  201. machine_definition_id = metadata["definition"]
  202. quality_type = metadata["quality_type"]
  203. if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
  204. self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
  205. machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
  206. machine_node.addQualityChangesMetadata(quality_type, metadata)
  207. Logger.log("d", "Lookup tables updated.")
  208. self.qualitiesUpdated.emit()
  209. def _updateMaps(self):
  210. self.initialize()
  211. def _onContainerMetadataChanged(self, container):
  212. self._onContainerChanged(container)
  213. def _onContainerChanged(self, container):
  214. container_type = container.getMetaDataEntry("type")
  215. if container_type not in ("quality", "quality_changes"):
  216. return
  217. # update the cache table
  218. self._update_timer.start()
  219. # Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
  220. def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
  221. used_extruders = set()
  222. # TODO: This will change after the Machine refactoring
  223. for i in range(machine.getProperty("machine_extruder_count", "value")):
  224. used_extruders.add(str(i))
  225. # Update the "is_available" flag for each quality group.
  226. for quality_group in quality_group_list:
  227. is_available = True
  228. if quality_group.node_for_global is None:
  229. is_available = False
  230. if is_available:
  231. for position in used_extruders:
  232. if position not in quality_group.nodes_for_extruders:
  233. is_available = False
  234. break
  235. quality_group.is_available = is_available
  236. # Returns a dict of "custom profile name" -> QualityChangesGroup
  237. def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
  238. # Get machine definition ID for quality search
  239. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  240. machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
  241. if not machine_node:
  242. Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
  243. return dict()
  244. # Update availability for each QualityChangesGroup:
  245. # A custom profile is always available as long as the quality_type it's based on is available
  246. quality_group_dict = self.getQualityGroups(machine)
  247. available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
  248. # Iterate over all quality_types in the machine node
  249. quality_changes_group_dict = dict()
  250. for quality_type, quality_changes_node in machine_node.quality_type_map.items():
  251. for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
  252. quality_changes_group_dict[quality_changes_name] = quality_changes_group
  253. quality_changes_group.is_available = quality_type in available_quality_type_list
  254. return quality_changes_group_dict
  255. def getQualityGroups(self, machine: "GlobalStack") -> dict:
  256. # Get machine definition ID for quality search
  257. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  258. # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
  259. has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
  260. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  261. # (1) the machine-specific node
  262. # (2) the generic node
  263. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  264. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(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 = {}
  268. for node in nodes_to_check:
  269. if node and node.quality_type_map:
  270. # Only include global qualities
  271. if has_variant_materials:
  272. quality_node = list(node.quality_type_map.values())[0]
  273. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  274. if not is_global_quality:
  275. continue
  276. for quality_type, quality_node in node.quality_type_map.items():
  277. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  278. quality_group.node_for_global = quality_node
  279. quality_group_dict[quality_type] = quality_group
  280. break
  281. # Iterate over all extruders to find quality containers for each extruder
  282. for position, extruder in machine.extruders.items():
  283. variant_name = None
  284. if extruder.variant.getId() != "empty_variant":
  285. variant_name = extruder.variant.getName()
  286. # This is a list of root material IDs to use for searching for suitable quality profiles.
  287. # The root material IDs in this list are in prioritized order.
  288. root_material_id_list = []
  289. has_material = False # flag indicating whether this extruder has a material assigned
  290. if extruder.material.getId() != "empty_material":
  291. has_material = True
  292. root_material_id = extruder.material.getMetaDataEntry("base_file")
  293. # Convert possible generic_pla_175 -> generic_pla
  294. root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
  295. root_material_id_list.append(root_material_id)
  296. # Also try to get the fallback material
  297. material_type = extruder.material.getMetaDataEntry("material")
  298. fallback_root_material_id = self._material_manager.getFallbackMaterialId(material_type)
  299. if fallback_root_material_id:
  300. root_material_id_list.append(fallback_root_material_id)
  301. nodes_to_check = []
  302. if variant_name:
  303. # In this case, we have both a specific variant and a specific material
  304. variant_node = machine_node.getChildNode(variant_name)
  305. if variant_node and has_material:
  306. for root_material_id in root_material_id_list:
  307. material_node = variant_node.getChildNode(root_material_id)
  308. if material_node:
  309. nodes_to_check.append(material_node)
  310. break
  311. nodes_to_check.append(variant_node)
  312. # In this case, we only have a specific material but NOT a variant
  313. if has_material:
  314. for root_material_id in root_material_id_list:
  315. material_node = machine_node.getChildNode(root_material_id)
  316. if material_node:
  317. nodes_to_check.append(material_node)
  318. break
  319. nodes_to_check += [machine_node, default_machine_node]
  320. for node in nodes_to_check:
  321. if node and node.quality_type_map:
  322. if has_variant_materials:
  323. # Only include variant qualities; skip non global qualities
  324. quality_node = list(node.quality_type_map.values())[0]
  325. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  326. if is_global_quality:
  327. continue
  328. for quality_type, quality_node in node.quality_type_map.items():
  329. if quality_type not in quality_group_dict:
  330. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  331. quality_group_dict[quality_type] = quality_group
  332. quality_group = quality_group_dict[quality_type]
  333. quality_group.nodes_for_extruders[position] = quality_node
  334. break
  335. # Update availabilities for each quality group
  336. self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
  337. return quality_group_dict
  338. def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
  339. # Get machine definition ID for quality search
  340. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  341. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  342. # (1) the machine-specific node
  343. # (2) the generic node
  344. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  345. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
  346. self._default_machine_definition_id)
  347. nodes_to_check = [machine_node, default_machine_node]
  348. # Iterate over all quality_types in the machine node
  349. quality_group_dict = dict()
  350. for node in nodes_to_check:
  351. if node and node.quality_type_map:
  352. for quality_type, quality_node in node.quality_type_map.items():
  353. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  354. quality_group.node_for_global = quality_node
  355. quality_group_dict[quality_type] = quality_group
  356. break
  357. return quality_group_dict
  358. #
  359. # Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
  360. # machine. The rule is as follows:
  361. # 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
  362. # machine.
  363. # 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
  364. # own machine definition ID for quality search.
  365. # Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
  366. # 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
  367. # definition ID specified in "quality_definition" should be used.
  368. # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
  369. # shares the same set of qualities profiles as Ultimaker 3.
  370. #
  371. def getMachineDefinitionIDForQualitySearch(machine: "GlobalStack", default_definition_id: str = "fdmprinter") -> str:
  372. machine_definition_id = default_definition_id
  373. if parseBool(machine.getMetaDataEntry("has_machine_quality", False)):
  374. # Only use the machine's own quality definition ID if this machine has machine quality.
  375. machine_definition_id = machine.getMetaDataEntry("quality_definition")
  376. if machine_definition_id is None:
  377. machine_definition_id = machine.definition.getId()
  378. return machine_definition_id