CuraContainerStack.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os.path
  4. from typing import Any, Optional
  5. from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
  6. from UM.FlameProfiler import pyqtSlot
  7. from UM.Decorators import override
  8. from UM.Logger import Logger
  9. from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
  10. from UM.Settings.InstanceContainer import InstanceContainer
  11. from UM.Settings.DefinitionContainer import DefinitionContainer
  12. from UM.Settings.ContainerRegistry import ContainerRegistry
  13. from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
  14. from . import Exceptions
  15. ## Base class for Cura related stacks that want to enforce certain containers are available.
  16. #
  17. # This class makes sure that the stack has the following containers set: user changes, quality
  18. # changes, quality, material, variant, definition changes and finally definition. Initially,
  19. # these will be equal to the empty instance container.
  20. #
  21. # The container types are determined based on the following criteria:
  22. # - user: An InstanceContainer with the metadata entry "type" set to "user".
  23. # - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
  24. # - quality: An InstanceContainer with the metadata entry "type" set to "quality".
  25. # - material: An InstanceContainer with the metadata entry "type" set to "material".
  26. # - variant: An InstanceContainer with the metadata entry "type" set to "variant".
  27. # - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
  28. # - definition: A DefinitionContainer.
  29. #
  30. # Internally, this class ensures the mentioned containers are always there and kept in a specific order.
  31. # This also means that operations on the stack that modifies the container ordering is prohibited and
  32. # will raise an exception.
  33. class CuraContainerStack(ContainerStack):
  34. def __init__(self, container_id: str, *args, **kwargs):
  35. super().__init__(container_id, *args, **kwargs)
  36. self._container_registry = ContainerRegistry.getInstance()
  37. self._empty_instance_container = self._container_registry.getEmptyInstanceContainer()
  38. self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
  39. self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
  40. self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
  41. self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0]
  42. self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))]
  43. self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
  44. self._containers[_ContainerIndexes.Quality] = self._empty_quality
  45. self._containers[_ContainerIndexes.Material] = self._empty_material
  46. self._containers[_ContainerIndexes.Variant] = self._empty_variant
  47. self.containersChanged.connect(self._onContainersChanged)
  48. import cura.CuraApplication #Here to prevent circular imports.
  49. self.addMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
  50. # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
  51. pyqtContainersChanged = pyqtSignal()
  52. ## Set the user changes container.
  53. #
  54. # \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user".
  55. def setUserChanges(self, new_user_changes: InstanceContainer) -> None:
  56. self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes)
  57. ## Get the user changes container.
  58. #
  59. # \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  60. @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
  61. def userChanges(self) -> InstanceContainer:
  62. return self._containers[_ContainerIndexes.UserChanges]
  63. ## Set the quality changes container.
  64. #
  65. # \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
  66. def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
  67. self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
  68. ## Set the quality changes container by an ID.
  69. #
  70. # This will search for the specified container and set it. If no container was found, an error will be raised.
  71. #
  72. # \param new_quality_changes_id The ID of the new quality changes container.
  73. #
  74. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  75. def setQualityChangesById(self, new_quality_changes_id: str) -> None:
  76. quality_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_changes_id)
  77. if quality_changes:
  78. self.setQualityChanges(quality_changes[0])
  79. else:
  80. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_changes_id))
  81. ## Get the quality changes container.
  82. #
  83. # \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  84. @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
  85. def qualityChanges(self) -> InstanceContainer:
  86. return self._containers[_ContainerIndexes.QualityChanges]
  87. ## Set the quality container.
  88. #
  89. # \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
  90. def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None:
  91. self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
  92. ## Set the quality container by an ID.
  93. #
  94. # This will search for the specified container and set it. If no container was found, an error will be raised.
  95. # There is a special value for ID, which is "default". The "default" value indicates the quality should be set
  96. # to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultQuality
  97. # for details.
  98. #
  99. # \param new_quality_id The ID of the new quality container.
  100. #
  101. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  102. def setQualityById(self, new_quality_id: str) -> None:
  103. quality = self._empty_quality
  104. if new_quality_id == "default":
  105. new_quality = self.findDefaultQuality()
  106. if new_quality:
  107. quality = new_quality
  108. else:
  109. qualities = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_id)
  110. if qualities:
  111. quality = qualities[0]
  112. else:
  113. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_id))
  114. self.setQuality(quality)
  115. ## Get the quality container.
  116. #
  117. # \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  118. @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
  119. def quality(self) -> InstanceContainer:
  120. return self._containers[_ContainerIndexes.Quality]
  121. ## Set the material container.
  122. #
  123. # \param new_quality_changes The new material container. It is expected to have a "type" metadata entry with the value "quality_changes".
  124. def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
  125. self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
  126. ## Set the material container by an ID.
  127. #
  128. # This will search for the specified container and set it. If no container was found, an error will be raised.
  129. # There is a special value for ID, which is "default". The "default" value indicates the quality should be set
  130. # to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultMaterial
  131. # for details.
  132. #
  133. # \param new_quality_changes_id The ID of the new material container.
  134. #
  135. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  136. def setMaterialById(self, new_material_id: str) -> None:
  137. material = self._empty_material
  138. if new_material_id == "default":
  139. new_material = self.findDefaultMaterial()
  140. if new_material:
  141. material = new_material
  142. else:
  143. materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
  144. if materials:
  145. material = materials[0]
  146. else:
  147. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_material_id))
  148. self.setMaterial(material)
  149. ## Get the material container.
  150. #
  151. # \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  152. @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
  153. def material(self) -> InstanceContainer:
  154. return self._containers[_ContainerIndexes.Material]
  155. ## Set the variant container.
  156. #
  157. # \param new_quality_changes The new variant container. It is expected to have a "type" metadata entry with the value "quality_changes".
  158. def setVariant(self, new_variant: InstanceContainer) -> None:
  159. self.replaceContainer(_ContainerIndexes.Variant, new_variant)
  160. ## Set the variant container by an ID.
  161. #
  162. # This will search for the specified container and set it. If no container was found, an error will be raised.
  163. # There is a special value for ID, which is "default". The "default" value indicates the quality should be set
  164. # to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultVariant
  165. # for details.
  166. #
  167. # \param new_quality_changes_id The ID of the new variant container.
  168. #
  169. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  170. def setVariantById(self, new_variant_id: str) -> None:
  171. variant = self._empty_variant
  172. if new_variant_id == "default":
  173. new_variant = self.findDefaultVariant()
  174. if new_variant:
  175. variant = new_variant
  176. else:
  177. variants = ContainerRegistry.getInstance().findInstanceContainers(id = new_variant_id)
  178. if variants:
  179. variant = variants[0]
  180. else:
  181. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_variant_id))
  182. self.setVariant(variant)
  183. ## Get the variant container.
  184. #
  185. # \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  186. @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
  187. def variant(self) -> InstanceContainer:
  188. return self._containers[_ContainerIndexes.Variant]
  189. ## Set the definition changes container.
  190. #
  191. # \param new_quality_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
  192. def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
  193. self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
  194. ## Set the definition changes container by an ID.
  195. #
  196. # \param new_quality_changes_id The ID of the new definition changes container.
  197. #
  198. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  199. def setDefinitionChangesById(self, new_definition_changes_id: str) -> None:
  200. new_definition_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_definition_changes_id)
  201. if new_definition_changes:
  202. self.setDefinitionChanges(new_definition_changes[0])
  203. else:
  204. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_changes_id))
  205. ## Get the definition changes container.
  206. #
  207. # \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  208. @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
  209. def definitionChanges(self) -> InstanceContainer:
  210. return self._containers[_ContainerIndexes.DefinitionChanges]
  211. ## Set the definition container.
  212. #
  213. # \param new_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes".
  214. def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
  215. self.replaceContainer(_ContainerIndexes.Definition, new_definition)
  216. ## Set the definition container by an ID.
  217. #
  218. # \param new_quality_changes_id The ID of the new definition container.
  219. #
  220. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
  221. def setDefinitionById(self, new_definition_id: str) -> None:
  222. new_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = new_definition_id)
  223. if new_definition:
  224. self.setDefinition(new_definition[0])
  225. else:
  226. raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_id))
  227. ## Get the definition container.
  228. #
  229. # \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
  230. @pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
  231. def definition(self) -> DefinitionContainer:
  232. return self._containers[_ContainerIndexes.Definition]
  233. @override(ContainerStack)
  234. def getBottom(self) -> "DefinitionContainer":
  235. return self.definition
  236. @override(ContainerStack)
  237. def getTop(self) -> "InstanceContainer":
  238. return self.userChanges
  239. ## Check whether the specified setting has a 'user' value.
  240. #
  241. # A user value here is defined as the setting having a value in either
  242. # the UserChanges or QualityChanges container.
  243. #
  244. # \return True if the setting has a user value, False if not.
  245. @pyqtSlot(str, result = bool)
  246. def hasUserValue(self, key: str) -> bool:
  247. if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"):
  248. return True
  249. if self._containers[_ContainerIndexes.QualityChanges].hasProperty(key, "value"):
  250. return True
  251. return False
  252. ## Set a property of a setting.
  253. #
  254. # This will set a property of a specified setting. Since the container stack does not contain
  255. # any settings itself, it is required to specify a container to set the property on. The target
  256. # container is matched by container type.
  257. #
  258. # \param key The key of the setting to set.
  259. # \param property_name The name of the property to set.
  260. # \param new_value The new value to set the property to.
  261. # \param target_container The type of the container to set the property of. Defaults to "user".
  262. def setProperty(self, key: str, property_name: str, new_value: Any, target_container: str = "user") -> None:
  263. container_index = _ContainerIndexes.TypeIndexMap.get(target_container, -1)
  264. if container_index != -1:
  265. self._containers[container_index].setProperty(key, property_name, new_value)
  266. else:
  267. raise IndexError("Invalid target container {type}".format(type = target_container))
  268. ## Overridden from ContainerStack
  269. #
  270. # Since we have a fixed order of containers in the stack and this method would modify the container
  271. # ordering, we disallow this operation.
  272. @override(ContainerStack)
  273. def addContainer(self, container: ContainerInterface) -> None:
  274. raise Exceptions.InvalidOperationError("Cannot add a container to Global stack")
  275. ## Overridden from ContainerStack
  276. #
  277. # Since we have a fixed order of containers in the stack and this method would modify the container
  278. # ordering, we disallow this operation.
  279. @override(ContainerStack)
  280. def insertContainer(self, index: int, container: ContainerInterface) -> None:
  281. raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack")
  282. ## Overridden from ContainerStack
  283. #
  284. # Since we have a fixed order of containers in the stack and this method would modify the container
  285. # ordering, we disallow this operation.
  286. @override(ContainerStack)
  287. def removeContainer(self, index: int = 0) -> None:
  288. raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack")
  289. ## Overridden from ContainerStack
  290. #
  291. # Replaces the container at the specified index with another container.
  292. # This version performs checks to make sure the new container has the expected metadata and type.
  293. #
  294. # \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
  295. @override(ContainerStack)
  296. def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None:
  297. expected_type = _ContainerIndexes.IndexTypeMap[index]
  298. if expected_type == "definition":
  299. if not isinstance(container, DefinitionContainer):
  300. raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not a DefinitionContainer".format(index = index))
  301. elif container != self._empty_instance_container and container.getMetaDataEntry("type") != expected_type:
  302. raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not of {type} type, but {actual_type} type.".format(index = index, type = expected_type, actual_type = container.getMetaDataEntry("type")))
  303. super().replaceContainer(index, container, postpone_emit)
  304. ## Overridden from ContainerStack
  305. #
  306. # This deserialize will make sure the internal list of containers matches with what we expect.
  307. # It will first check to see if the container at a certain index already matches with what we
  308. # expect. If it does not, it will search for a matching container with the correct type. Should
  309. # no container with the correct type be found, it will use the empty container.
  310. #
  311. # \throws InvalidContainerStackError Raised when no definition can be found for the stack.
  312. @override(ContainerStack)
  313. def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
  314. super().deserialize(contents, file_name)
  315. new_containers = self._containers.copy()
  316. while len(new_containers) < len(_ContainerIndexes.IndexTypeMap):
  317. new_containers.append(self._empty_instance_container)
  318. # Validate and ensure the list of containers matches with what we expect
  319. for index, type_name in _ContainerIndexes.IndexTypeMap.items():
  320. try:
  321. container = new_containers[index]
  322. except IndexError:
  323. container = None
  324. if type_name == "definition":
  325. if not container or not isinstance(container, DefinitionContainer):
  326. definition = self.findContainer(container_type = DefinitionContainer)
  327. if not definition:
  328. raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self.getId()))
  329. new_containers[index] = definition
  330. continue
  331. if not container or container.getMetaDataEntry("type") != type_name:
  332. actual_container = self.findContainer(type = type_name)
  333. if actual_container:
  334. new_containers[index] = actual_container
  335. else:
  336. new_containers[index] = self._empty_instance_container
  337. self._containers = new_containers
  338. ## Find the variant that should be used as "default" variant.
  339. #
  340. # This will search for variants that match the current definition and pick the preferred one,
  341. # if specified by the machine definition.
  342. #
  343. # The following criteria are used to find the default variant:
  344. # - If the machine definition does not have a metadata entry "has_variants" set to True, return None
  345. # - The definition of the variant should be the same as the machine definition for this stack.
  346. # - The container should have a metadata entry "type" with value "variant".
  347. # - If the machine definition has a metadata entry "preferred_variant", filter the variant IDs based on that.
  348. #
  349. # \return The container that should be used as default, or None if nothing was found or the machine does not use variants.
  350. #
  351. # \note This method assumes the stack has a valid machine definition.
  352. def findDefaultVariant(self) -> Optional[ContainerInterface]:
  353. definition = self._getMachineDefinition()
  354. # has_variants can be overridden in other containers and stacks.
  355. # In the case of UM2, it is overridden in the GlobalStack
  356. if not self.getMetaDataEntry("has_variants"):
  357. # If the machine does not use variants, we should never set a variant.
  358. return None
  359. # First add any variant. Later, overwrite with preference if the preference is valid.
  360. variant = None
  361. definition_id = self._findInstanceContainerDefinitionId(definition)
  362. variants = ContainerRegistry.getInstance().findInstanceContainers(definition = definition_id, type = "variant")
  363. if variants:
  364. variant = variants[0]
  365. preferred_variant_id = definition.getMetaDataEntry("preferred_variant")
  366. if preferred_variant_id:
  367. preferred_variants = ContainerRegistry.getInstance().findInstanceContainers(id = preferred_variant_id, definition = definition_id, type = "variant")
  368. if preferred_variants:
  369. variant = preferred_variants[0]
  370. else:
  371. Logger.log("w", "The preferred variant \"{variant}\" of stack {stack} does not exist or is not a variant.", variant = preferred_variant_id, stack = self.id)
  372. # And leave it at the default variant.
  373. if variant:
  374. return variant
  375. Logger.log("w", "Could not find a valid default variant for stack {stack}", stack = self.id)
  376. return None
  377. ## Find the material that should be used as "default" material.
  378. #
  379. # This will search for materials that match the current definition and pick the preferred one,
  380. # if specified by the machine definition.
  381. #
  382. # The following criteria are used to find the default material:
  383. # - If the machine definition does not have a metadata entry "has_materials" set to True, return None
  384. # - If the machine definition has a metadata entry "has_machine_materials", the definition of the material should
  385. # be the same as the machine definition for this stack. Otherwise, the definition should be "fdmprinter".
  386. # - The container should have a metadata entry "type" with value "material".
  387. # - The material should have an approximate diameter that matches the machine
  388. # - If the machine definition has a metadata entry "has_variants" and set to True, the "variant" metadata entry of
  389. # the material should be the same as the ID of the variant in the stack. Only applies if "has_machine_materials" is also True.
  390. # - If the stack currently has a material set, try to find a material that matches the current material by name.
  391. # - Otherwise, if the machine definition has a metadata entry "preferred_material", try to find a material that matches the specified ID.
  392. #
  393. # \return The container that should be used as default, or None if nothing was found or the machine does not use materials.
  394. def findDefaultMaterial(self) -> Optional[ContainerInterface]:
  395. definition = self._getMachineDefinition()
  396. if not definition.getMetaDataEntry("has_materials"):
  397. # Machine does not use materials, never try to set it.
  398. return None
  399. search_criteria = {"type": "material"}
  400. if definition.getMetaDataEntry("has_machine_materials"):
  401. search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
  402. if definition.getMetaDataEntry("has_variants"):
  403. search_criteria["variant"] = self.variant.id
  404. else:
  405. search_criteria["definition"] = "fdmprinter"
  406. if self.material != self._empty_material:
  407. search_criteria["name"] = self.material.name
  408. else:
  409. preferred_material = definition.getMetaDataEntry("preferred_material")
  410. if preferred_material:
  411. search_criteria["id"] = preferred_material
  412. approximate_material_diameter = str(round(self.getProperty("material_diameter", "value")))
  413. search_criteria["approximate_diameter"] = approximate_material_diameter
  414. materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  415. if not materials:
  416. Logger.log("w", "The preferred material \"{material}\" could not be found for stack {stack}", material = preferred_material, stack = self.id)
  417. # We failed to find any materials matching the specified criteria, drop some specific criteria and try to find
  418. # a material that sort-of matches what we want.
  419. search_criteria.pop("variant", None)
  420. search_criteria.pop("id", None)
  421. search_criteria.pop("name", None)
  422. materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  423. if not materials:
  424. Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
  425. return None
  426. for material in materials:
  427. # Prefer a read-only material
  428. if ContainerRegistry.getInstance().isReadOnly(material.getId()):
  429. return material
  430. return materials[0]
  431. ## Find the quality that should be used as "default" quality.
  432. #
  433. # This will search for qualities that match the current definition and pick the preferred one,
  434. # if specified by the machine definition.
  435. #
  436. # \return The container that should be used as default, or None if nothing was found.
  437. def findDefaultQuality(self) -> Optional[ContainerInterface]:
  438. definition = self._getMachineDefinition()
  439. registry = ContainerRegistry.getInstance()
  440. material_container = self.material if self.material.getId() not in (self._empty_material.getId(), self._empty_instance_container.getId()) else None
  441. search_criteria = {"type": "quality"}
  442. if definition.getMetaDataEntry("has_machine_quality"):
  443. search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
  444. if definition.getMetaDataEntry("has_materials") and material_container:
  445. search_criteria["material"] = material_container.id
  446. else:
  447. search_criteria["definition"] = "fdmprinter"
  448. if self.quality != self._empty_quality:
  449. search_criteria["name"] = self.quality.name
  450. else:
  451. preferred_quality = definition.getMetaDataEntry("preferred_quality")
  452. if preferred_quality:
  453. search_criteria["id"] = preferred_quality
  454. containers = registry.findInstanceContainers(**search_criteria)
  455. if containers:
  456. return containers[0]
  457. if "material" in search_criteria:
  458. # First check if we can solve our material not found problem by checking if we can find quality containers
  459. # that are assigned to the parents of this material profile.
  460. try:
  461. inherited_files = material_container.getInheritedFiles()
  462. except AttributeError: # Material_container does not support inheritance.
  463. inherited_files = []
  464. if inherited_files:
  465. for inherited_file in inherited_files:
  466. # Extract the ID from the path we used to load the file.
  467. search_criteria["material"] = os.path.basename(inherited_file).split(".")[0]
  468. containers = registry.findInstanceContainers(**search_criteria)
  469. if containers:
  470. return containers[0]
  471. # We still weren't able to find a quality for this specific material.
  472. # Try to find qualities for a generic version of the material.
  473. material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
  474. if definition.getMetaDataEntry("has_machine_quality"):
  475. if self.material != self._empty_instance_container:
  476. material_search_criteria["definition"] = material_container.getMetaDataEntry("definition")
  477. if definition.getMetaDataEntry("has_variants"):
  478. material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
  479. else:
  480. material_search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
  481. if definition.getMetaDataEntry("has_variants") and self.variant != self._empty_instance_container:
  482. material_search_criteria["variant"] = self.variant.id
  483. else:
  484. material_search_criteria["definition"] = "fdmprinter"
  485. material_containers = registry.findInstanceContainersMetadata(**material_search_criteria)
  486. # Try all materials to see if there is a quality profile available.
  487. for material_container in material_containers:
  488. search_criteria["material"] = material_container["id"]
  489. containers = registry.findInstanceContainers(**search_criteria)
  490. if containers:
  491. return containers[0]
  492. if "name" in search_criteria or "id" in search_criteria:
  493. # If a quality by this name can not be found, try a wider set of search criteria
  494. search_criteria.pop("name", None)
  495. search_criteria.pop("id", None)
  496. containers = registry.findInstanceContainers(**search_criteria)
  497. if containers:
  498. return containers[0]
  499. return None
  500. ## protected:
  501. # Helper to make sure we emit a PyQt signal on container changes.
  502. def _onContainersChanged(self, container: Any) -> None:
  503. self.pyqtContainersChanged.emit()
  504. # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
  505. # and its properties rather than, for example, the extruder. Defaults to simply returning the definition property.
  506. def _getMachineDefinition(self) -> DefinitionContainer:
  507. return self.definition
  508. ## Find the ID that should be used when searching for instance containers for a specified definition.
  509. #
  510. # This handles the situation where the definition specifies we should use a different definition when
  511. # searching for instance containers.
  512. #
  513. # \param machine_definition The definition to find the "quality definition" for.
  514. #
  515. # \return The ID of the definition container to use when searching for instance containers.
  516. @classmethod
  517. def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainer) -> str:
  518. quality_definition = machine_definition.getMetaDataEntry("quality_definition")
  519. if not quality_definition:
  520. return machine_definition.id
  521. definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition)
  522. if not definitions:
  523. Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id)
  524. return machine_definition.id
  525. return cls._findInstanceContainerDefinitionId(definitions[0])
  526. ## private:
  527. # Private helper class to keep track of container positions and their types.
  528. class _ContainerIndexes:
  529. UserChanges = 0
  530. QualityChanges = 1
  531. Quality = 2
  532. Material = 3
  533. Variant = 4
  534. DefinitionChanges = 5
  535. Definition = 6
  536. # Simple hash map to map from index to "type" metadata entry
  537. IndexTypeMap = {
  538. UserChanges: "user",
  539. QualityChanges: "quality_changes",
  540. Quality: "quality",
  541. Material: "material",
  542. Variant: "variant",
  543. DefinitionChanges: "definition_changes",
  544. Definition: "definition",
  545. }
  546. # Reverse lookup: type -> index
  547. TypeIndexMap = dict([(v, k) for k, v in IndexTypeMap.items()])