MaterialManagementModel.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. with postponeSignals(container_registry.containerAdded, compress=CompressTechnique.CompressPerParameterValue):
  142. for container_to_add in new_containers:
  143. container_to_add.setDirty(True)
  144. container_registry.addContainer(container_to_add)
  145. # If the duplicated material was favorite then the new material should also be added to the favorites.
  146. favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
  147. if base_file in favorites_set:
  148. favorites_set.add(new_base_id)
  149. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
  150. return new_base_id
  151. ## Creates a duplicate of a material with the same GUID and base_file
  152. # metadata.
  153. # \param material_node The node representing the material to duplicate.
  154. # \param new_base_id A new material ID for the base material. The IDs of
  155. # the submaterials will be based off this one. If not provided, a material
  156. # ID will be generated automatically.
  157. # \param new_metadata Metadata for the new material. If not provided, this
  158. # will be duplicated from the original material.
  159. # \return The root material ID of the duplicate material.
  160. @pyqtSlot("QVariant", result = str)
  161. def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None,
  162. new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
  163. return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
  164. ## Create a new material by cloning the preferred material for the current
  165. # material diameter and generate a new GUID.
  166. #
  167. # The material type is explicitly left to be the one from the preferred
  168. # material, since this allows the user to still have SOME profiles to work
  169. # with.
  170. # \return The ID of the newly created material.
  171. @pyqtSlot(result = str)
  172. def createMaterial(self) -> str:
  173. # Ensure all settings are saved.
  174. application = cura.CuraApplication.CuraApplication.getInstance()
  175. application.saveSettings()
  176. # Find the preferred material.
  177. extruder_stack = application.getMachineManager().activeStack
  178. active_variant_name = extruder_stack.variant.getName()
  179. approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
  180. global_container_stack = application.getGlobalContainerStack()
  181. if not global_container_stack:
  182. return ""
  183. machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
  184. preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
  185. # Create a new ID & new metadata for the new material.
  186. new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
  187. new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
  188. "brand": catalog.i18nc("@label", "Custom"),
  189. "GUID": str(uuid.uuid4()),
  190. }
  191. self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
  192. return new_id
  193. ## Adds a certain material to the favorite materials.
  194. # \param material_base_file The base file of the material to add.
  195. @pyqtSlot(str)
  196. def addFavorite(self, material_base_file: str) -> None:
  197. application = cura.CuraApplication.CuraApplication.getInstance()
  198. favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
  199. if material_base_file not in favorites:
  200. favorites.append(material_base_file)
  201. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
  202. application.saveSettings()
  203. self.favoritesChanged.emit(material_base_file)
  204. ## Removes a certain material from the favorite materials.
  205. #
  206. # If the material was not in the favorite materials, nothing happens.
  207. @pyqtSlot(str)
  208. def removeFavorite(self, material_base_file: str) -> None:
  209. application = cura.CuraApplication.CuraApplication.getInstance()
  210. favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
  211. try:
  212. favorites.remove(material_base_file)
  213. application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
  214. application.saveSettings()
  215. self.favoritesChanged.emit(material_base_file)
  216. except ValueError: # Material was not in the favorites list.
  217. Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))