MaterialManagementModel.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import copy # To duplicate materials.
  4. from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
  5. from typing import Any, Dict, Optional, TYPE_CHECKING
  6. import uuid # To generate new GUIDs for new materials.
  7. from UM.i18n import i18nCatalog
  8. from UM.Logger import Logger
  9. from UM.Signal import postponeSignals, CompressTechnique
  10. import cura.CuraApplication # Imported like this to prevent circular imports.
  11. from cura.Machines.ContainerTree import ContainerTree
  12. from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
  13. if TYPE_CHECKING:
  14. from cura.Machines.MaterialNode import MaterialNode
  15. catalog = i18nCatalog("cura")
  16. ## Proxy class to the materials page in the preferences.
  17. #
  18. # This class handles the actions in that page, such as creating new materials,
  19. # renaming them, etc.
  20. class MaterialManagementModel(QObject):
  21. ## Triggered when a favorite is added or removed.
  22. # \param The base file of the material is provided as parameter when this
  23. # emits.
  24. favoritesChanged = pyqtSignal(str)
  25. ## Can a certain material be deleted, or is it still in use in one of the
  26. # container stacks anywhere?
  27. #
  28. # We forbid the user from deleting a material if it's in use in any stack.
  29. # Deleting it while it's in use can lead to corrupted stacks. In the
  30. # future we might enable this functionality again (deleting the material
  31. # from those stacks) but for now it is easier to prevent the user from
  32. # doing this.
  33. # \param material_node The ContainerTree node of the material to check.
  34. # \return Whether or not the material can be removed.
  35. @pyqtSlot("QVariant", result = bool)
  36. def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
  37. container_registry = CuraContainerRegistry.getInstance()
  38. ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
  39. for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
  40. if extruder_stack.material.getId() in ids_to_remove:
  41. return False
  42. return True
  43. ## Change the user-visible name of a material.
  44. # \param material_node The ContainerTree node of the material to rename.
  45. # \param name The new name for the material.
  46. @pyqtSlot("QVariant", str)
  47. def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
  48. container_registry = CuraContainerRegistry.getInstance()
  49. root_material_id = material_node.base_file
  50. if container_registry.isReadOnly(root_material_id):
  51. Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
  52. return
  53. return container_registry.findContainers(id = root_material_id)[0].setName(name)
  54. ## Deletes a material from Cura.
  55. #
  56. # This function does not do any safety checking any more. Please call this
  57. # function only if:
  58. # - The material is not read-only.
  59. # - The material is not used in any stacks.
  60. # If the material was not lazy-loaded yet, this will fully load the
  61. # container. When removing this material node, all other materials with
  62. # the same base fill will also be removed.
  63. # \param material_node The material to remove.
  64. @pyqtSlot("QVariant")
  65. def removeMaterial(self, material_node: "MaterialNode") -> None:
  66. container_registry = CuraContainerRegistry.getInstance()
  67. materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
  68. # The material containers belonging to the same material file are supposed to work together. This postponeSignals()
  69. # does two things:
  70. # - optimizing the signal emitting.
  71. # - making sure that the signals will only be emitted after all the material containers have been removed.
  72. with postponeSignals(container_registry.containerRemoved, compress = CompressTechnique.CompressPerParameterValue):
  73. # CURA-6886: Some containers may not have been loaded. If remove one material container, its material file
  74. # will be removed. If later we remove a sub-material container which hasn't been loaded previously, it will
  75. # crash because removeContainer() requires to load the container first, but the material file was already
  76. # gone.
  77. for material_metadata in materials_this_base_file:
  78. container_registry.findInstanceContainers(id = material_metadata["id"])
  79. for material_metadata in materials_this_base_file:
  80. container_registry.removeContainer(material_metadata["id"])
  81. ## Creates a duplicate of a material with the same GUID and base_file
  82. # metadata.
  83. # \param base_file: The base file of the material to duplicate.
  84. # \param new_base_id A new material ID for the base material. The IDs of
  85. # the submaterials will be based off this one. If not provided, a material
  86. # ID will be generated automatically.
  87. # \param new_metadata Metadata for the new material. If not provided, this
  88. # will be duplicated from the original material.
  89. # \return The root material ID of the duplicate material.
  90. def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None,
  91. new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
  92. container_registry = CuraContainerRegistry.getInstance()
  93. root_materials = container_registry.findContainers(id = base_file)
  94. if not root_materials:
  95. Logger.log("i", "Unable to duplicate the root material with ID {root_id}, because it doesn't exist.".format(root_id = base_file))
  96. return None
  97. root_material = root_materials[0]
  98. # Ensure that all settings are saved.
  99. application = cura.CuraApplication.CuraApplication.getInstance()
  100. application.saveSettings()
  101. # Create a new ID and container to hold the data.
  102. if new_base_id is None:
  103. new_base_id = container_registry.uniqueName(root_material.getId())
  104. new_root_material = copy.deepcopy(root_material)
  105. new_root_material.getMetaData()["id"] = new_base_id
  106. new_root_material.getMetaData()["base_file"] = new_base_id
  107. if new_metadata is not None:
  108. new_root_material.getMetaData().update(new_metadata)
  109. new_containers = [new_root_material]
  110. # Clone all submaterials.
  111. for container_to_copy in container_registry.findInstanceContainers(base_file = base_file):
  112. if container_to_copy.getId() == base_file:
  113. continue # We already have that one. Skip it.
  114. new_id = new_base_id
  115. definition = container_to_copy.getMetaDataEntry("definition")
  116. if definition != "fdmprinter":
  117. new_id += "_" + definition
  118. variant_name = container_to_copy.getMetaDataEntry("variant_name")
  119. if variant_name:
  120. new_id += "_" + variant_name.replace(" ", "_")
  121. new_container = copy.deepcopy(container_to_copy)
  122. new_container.getMetaData()["id"] = new_id
  123. new_container.getMetaData()["base_file"] = new_base_id
  124. if new_metadata is not None:
  125. new_container.getMetaData().update(new_metadata)
  126. new_containers.append(new_container)
  127. # CURA-6863: Nodes in ContainerTree will be updated upon ContainerAdded signals, one at a time. It will use the
  128. # best fit material container at the time it sees one. For example, if you duplicate and get generic_pva #2,
  129. # if the node update function sees the containers in the following order:
  130. #
  131. # - generic_pva #2
  132. # - generic_pva #2_um3_aa04
  133. #
  134. # It will first use "generic_pva #2" because that's the best fit it has ever seen, and later "generic_pva #2_um3_aa04"
  135. # once it sees that. Because things run in the Qt event loop, they don't happen at the same time. This means if
  136. # between those two events, the ContainerTree will have nodes that contain invalid data.
  137. #
  138. # This sort fixes the problem by emitting the most specific containers first.
  139. new_containers = sorted(new_containers, key = lambda x: x.getId(), reverse = True)
  140. # Optimization. Serving the same purpose as the postponeSignals() in removeMaterial()
  141. # postpone the signals emitted when duplicating materials. This is easier on the event loop; changes the
  142. # behavior to be like a transaction. Prevents concurrency issues.
  143. with postponeSignals(container_registry.containerAdded, compress=CompressTechnique.CompressPerParameterValue):
  144. for container_to_add in new_containers:
  145. container_to_add.setDirty(True)
  146. container_registry.addContainer(container_to_add)
  147. # If the duplicated material was favorite then the new material should also be added to the favorites.
  148. favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
  149. if base_file in favorites_set:
  150. favorites_set.add(new_base_id)
  151. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
  152. return new_base_id
  153. ## Creates a duplicate of a material with the same GUID and base_file
  154. # metadata.
  155. # \param material_node The node representing the material to duplicate.
  156. # \param new_base_id A new material ID for the base material. The IDs of
  157. # the submaterials will be based off this one. If not provided, a material
  158. # ID will be generated automatically.
  159. # \param new_metadata Metadata for the new material. If not provided, this
  160. # will be duplicated from the original material.
  161. # \return The root material ID of the duplicate material.
  162. @pyqtSlot("QVariant", result = str)
  163. def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None,
  164. new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
  165. return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
  166. ## Create a new material by cloning the preferred material for the current
  167. # material diameter and generate a new GUID.
  168. #
  169. # The material type is explicitly left to be the one from the preferred
  170. # material, since this allows the user to still have SOME profiles to work
  171. # with.
  172. # \return The ID of the newly created material.
  173. @pyqtSlot(result = str)
  174. def createMaterial(self) -> str:
  175. # Ensure all settings are saved.
  176. application = cura.CuraApplication.CuraApplication.getInstance()
  177. application.saveSettings()
  178. # Find the preferred material.
  179. extruder_stack = application.getMachineManager().activeStack
  180. active_variant_name = extruder_stack.variant.getName()
  181. approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
  182. global_container_stack = application.getGlobalContainerStack()
  183. if not global_container_stack:
  184. return ""
  185. machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
  186. preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
  187. # Create a new ID & new metadata for the new material.
  188. new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
  189. new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
  190. "brand": catalog.i18nc("@label", "Custom"),
  191. "GUID": str(uuid.uuid4()),
  192. }
  193. self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
  194. return new_id
  195. ## Adds a certain material to the favorite materials.
  196. # \param material_base_file The base file of the material to add.
  197. @pyqtSlot(str)
  198. def addFavorite(self, material_base_file: str) -> None:
  199. application = cura.CuraApplication.CuraApplication.getInstance()
  200. favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
  201. if material_base_file not in favorites:
  202. favorites.append(material_base_file)
  203. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
  204. application.saveSettings()
  205. self.favoritesChanged.emit(material_base_file)
  206. ## Removes a certain material from the favorite materials.
  207. #
  208. # If the material was not in the favorite materials, nothing happens.
  209. @pyqtSlot(str)
  210. def removeFavorite(self, material_base_file: str) -> None:
  211. application = cura.CuraApplication.CuraApplication.getInstance()
  212. favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
  213. try:
  214. favorites.remove(material_base_file)
  215. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
  216. application.saveSettings()
  217. self.favoritesChanged.emit(material_base_file)
  218. except ValueError: # Material was not in the favorites list.
  219. Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))