QualityManager.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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. #
  123. # Similar to MaterialManager, QualityManager maintains a number of maps and trees for material lookup.
  124. # The models GUI and QML use are now only dependent on the QualityManager. That means as long as the data in
  125. # QualityManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
  126. #
  127. # For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
  128. # again. This means the update is exactly the same as initialization. There are performance concerns about this approach
  129. # but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
  130. # because it's simple.
  131. #
  132. class QualityManager(QObject):
  133. qualitiesUpdated = pyqtSignal()
  134. def __init__(self, container_registry, parent = None):
  135. super().__init__(parent)
  136. self._application = Application.getInstance()
  137. self._material_manager = self._application._material_manager
  138. self._container_registry = container_registry
  139. self._empty_quality_container = self._application.empty_quality_container
  140. self._empty_quality_changes_container = self._application.empty_quality_changes_container
  141. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  142. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  143. self._default_machine_definition_id = "fdmprinter"
  144. self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
  145. self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
  146. self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
  147. # When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
  148. # we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
  149. # we don't react too many time.
  150. self._update_timer = QTimer(self)
  151. self._update_timer.setInterval(300)
  152. self._update_timer.setSingleShot(True)
  153. self._update_timer.timeout.connect(self._updateMaps)
  154. def initialize(self):
  155. # Initialize the lookup tree for quality profiles with following structure:
  156. # <machine> -> <variant> -> <material>
  157. # -> <material>
  158. self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
  159. self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
  160. quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
  161. for metadata in quality_metadata_list:
  162. if metadata["id"] == "empty_quality":
  163. continue
  164. definition_id = metadata["definition"]
  165. quality_type = metadata["quality_type"]
  166. root_material_id = metadata.get("material")
  167. variant_name = metadata.get("variant")
  168. is_global_quality = metadata.get("global_quality", False)
  169. is_global_quality = is_global_quality or (root_material_id is None and variant_name is None)
  170. # Sanity check: material+variant and is_global_quality cannot be present at the same time
  171. if is_global_quality and (root_material_id or variant_name):
  172. raise RuntimeError("Quality profile [%s] contains invalid data: it is a global quality but contains 'material' and 'nozzle' info." % metadata["id"])
  173. if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
  174. self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
  175. machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id]
  176. if is_global_quality:
  177. # For global qualities, save data in the machine node
  178. machine_node.addQualityMetadata(quality_type, metadata)
  179. continue
  180. if variant_name is not None:
  181. # If variant_name is specified in the quality/quality_changes profile, check if material is specified,
  182. # too.
  183. if variant_name not in machine_node.children_map:
  184. machine_node.children_map[variant_name] = QualityNode()
  185. variant_node = machine_node.children_map[variant_name]
  186. if root_material_id is None:
  187. # If only variant_name is specified but material is not, add the quality/quality_changes metadata
  188. # into the current variant node.
  189. variant_node.addQualityMetadata(quality_type, metadata)
  190. else:
  191. # If only variant_name and material are both specified, go one level deeper: create a material node
  192. # under the current variant node, and then add the quality/quality_changes metadata into the
  193. # material node.
  194. if root_material_id not in variant_node.children_map:
  195. variant_node.children_map[root_material_id] = QualityNode()
  196. material_node = variant_node.children_map[root_material_id]
  197. material_node.addQualityMetadata(quality_type, metadata)
  198. else:
  199. # If variant_name is not specified, check if material is specified.
  200. if root_material_id is not None:
  201. if root_material_id not in machine_node.children_map:
  202. machine_node.children_map[root_material_id] = QualityNode()
  203. material_node = machine_node.children_map[root_material_id]
  204. material_node.addQualityMetadata(quality_type, metadata)
  205. # Initialize the lookup tree for quality_changes profiles with following structure:
  206. # <machine> -> <quality_type> -> <name>
  207. quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
  208. for metadata in quality_changes_metadata_list:
  209. if metadata["id"] == "empty_quality_changes":
  210. continue
  211. machine_definition_id = metadata["definition"]
  212. quality_type = metadata["quality_type"]
  213. if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
  214. self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
  215. machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
  216. machine_node.addQualityChangesMetadata(quality_type, metadata)
  217. Logger.log("d", "Lookup tables updated.")
  218. self.qualitiesUpdated.emit()
  219. def _updateMaps(self):
  220. self.initialize()
  221. def _onContainerMetadataChanged(self, container):
  222. self._onContainerChanged(container)
  223. def _onContainerChanged(self, container):
  224. container_type = container.getMetaDataEntry("type")
  225. if container_type not in ("quality", "quality_changes"):
  226. return
  227. # update the cache table
  228. self._update_timer.start()
  229. # Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
  230. def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
  231. used_extruders = set()
  232. for i in range(machine.getProperty("machine_extruder_count", "value")):
  233. if machine.extruders[str(i)].isEnabled:
  234. used_extruders.add(str(i))
  235. # Update the "is_available" flag for each quality group.
  236. for quality_group in quality_group_list:
  237. is_available = True
  238. if quality_group.node_for_global is None:
  239. is_available = False
  240. if is_available:
  241. for position in used_extruders:
  242. if position not in quality_group.nodes_for_extruders:
  243. is_available = False
  244. break
  245. quality_group.is_available = is_available
  246. # Returns a dict of "custom profile name" -> QualityChangesGroup
  247. def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
  248. # Get machine definition ID for quality search
  249. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  250. machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
  251. if not machine_node:
  252. Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
  253. return dict()
  254. # Update availability for each QualityChangesGroup:
  255. # A custom profile is always available as long as the quality_type it's based on is available
  256. quality_group_dict = self.getQualityGroups(machine)
  257. available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
  258. # Iterate over all quality_types in the machine node
  259. quality_changes_group_dict = dict()
  260. for quality_type, quality_changes_node in machine_node.quality_type_map.items():
  261. for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
  262. quality_changes_group_dict[quality_changes_name] = quality_changes_group
  263. quality_changes_group.is_available = quality_type in available_quality_type_list
  264. return quality_changes_group_dict
  265. def getQualityGroups(self, machine: "GlobalStack") -> dict:
  266. # Get machine definition ID for quality search
  267. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  268. # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
  269. has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
  270. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  271. # (1) the machine-specific node
  272. # (2) the generic node
  273. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  274. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
  275. nodes_to_check = [machine_node, default_machine_node]
  276. # Iterate over all quality_types in the machine node
  277. quality_group_dict = {}
  278. for node in nodes_to_check:
  279. if node and node.quality_type_map:
  280. # Only include global qualities
  281. if has_variant_materials:
  282. quality_node = list(node.quality_type_map.values())[0]
  283. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  284. if not is_global_quality:
  285. continue
  286. for quality_type, quality_node in node.quality_type_map.items():
  287. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  288. quality_group.node_for_global = quality_node
  289. quality_group_dict[quality_type] = quality_group
  290. break
  291. # Iterate over all extruders to find quality containers for each extruder
  292. for position, extruder in machine.extruders.items():
  293. variant_name = None
  294. if extruder.variant.getId() != "empty_variant":
  295. variant_name = extruder.variant.getName()
  296. # This is a list of root material IDs to use for searching for suitable quality profiles.
  297. # The root material IDs in this list are in prioritized order.
  298. root_material_id_list = []
  299. has_material = False # flag indicating whether this extruder has a material assigned
  300. if extruder.material.getId() != "empty_material":
  301. has_material = True
  302. root_material_id = extruder.material.getMetaDataEntry("base_file")
  303. # Convert possible generic_pla_175 -> generic_pla
  304. root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
  305. root_material_id_list.append(root_material_id)
  306. # Also try to get the fallback material
  307. material_type = extruder.material.getMetaDataEntry("material")
  308. fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
  309. if fallback_root_material_id:
  310. root_material_id_list.append(fallback_root_material_id)
  311. nodes_to_check = []
  312. if variant_name:
  313. # In this case, we have both a specific variant and a specific material
  314. variant_node = machine_node.getChildNode(variant_name)
  315. if variant_node and has_material:
  316. for root_material_id in root_material_id_list:
  317. material_node = variant_node.getChildNode(root_material_id)
  318. if material_node:
  319. nodes_to_check.append(material_node)
  320. break
  321. nodes_to_check.append(variant_node)
  322. # In this case, we only have a specific material but NOT a variant
  323. if has_material:
  324. for root_material_id in root_material_id_list:
  325. material_node = machine_node.getChildNode(root_material_id)
  326. if material_node:
  327. nodes_to_check.append(material_node)
  328. break
  329. nodes_to_check += [machine_node, default_machine_node]
  330. for node in nodes_to_check:
  331. if node and node.quality_type_map:
  332. if has_variant_materials:
  333. # Only include variant qualities; skip non global qualities
  334. quality_node = list(node.quality_type_map.values())[0]
  335. is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
  336. if is_global_quality:
  337. continue
  338. for quality_type, quality_node in node.quality_type_map.items():
  339. if quality_type not in quality_group_dict:
  340. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  341. quality_group_dict[quality_type] = quality_group
  342. quality_group = quality_group_dict[quality_type]
  343. quality_group.nodes_for_extruders[position] = quality_node
  344. break
  345. # Update availabilities for each quality group
  346. self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
  347. return quality_group_dict
  348. def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
  349. # Get machine definition ID for quality search
  350. machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
  351. # To find the quality container for the GlobalStack, check in the following fall-back manner:
  352. # (1) the machine-specific node
  353. # (2) the generic node
  354. machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
  355. default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
  356. self._default_machine_definition_id)
  357. nodes_to_check = [machine_node, default_machine_node]
  358. # Iterate over all quality_types in the machine node
  359. quality_group_dict = dict()
  360. for node in nodes_to_check:
  361. if node and node.quality_type_map:
  362. for quality_type, quality_node in node.quality_type_map.items():
  363. quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
  364. quality_group.node_for_global = quality_node
  365. quality_group_dict[quality_type] = quality_group
  366. break
  367. return quality_group_dict
  368. #
  369. # Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
  370. # machine. The rule is as follows:
  371. # 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
  372. # machine.
  373. # 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
  374. # own machine definition ID for quality search.
  375. # Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
  376. # 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
  377. # definition ID specified in "quality_definition" should be used.
  378. # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
  379. # shares the same set of qualities profiles as Ultimaker 3.
  380. #
  381. def getMachineDefinitionIDForQualitySearch(machine: "GlobalStack", default_definition_id: str = "fdmprinter") -> str:
  382. machine_definition_id = default_definition_id
  383. if parseBool(machine.getMetaDataEntry("has_machine_quality", False)):
  384. # Only use the machine's own quality definition ID if this machine has machine quality.
  385. machine_definition_id = machine.getMetaDataEntry("quality_definition")
  386. if machine_definition_id is None:
  387. machine_definition_id = machine.definition.getId()
  388. return machine_definition_id