MaterialNode.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Any, Optional, TYPE_CHECKING
  4. from UM.Logger import Logger
  5. from UM.Settings.ContainerRegistry import ContainerRegistry
  6. from UM.Settings.Interfaces import ContainerInterface
  7. from UM.Signal import Signal
  8. from cura.Machines.ContainerNode import ContainerNode
  9. from cura.Machines.QualityNode import QualityNode
  10. import UM.FlameProfiler
  11. if TYPE_CHECKING:
  12. from typing import Dict
  13. from cura.Machines.VariantNode import VariantNode
  14. class MaterialNode(ContainerNode):
  15. """Represents a material in the container tree.
  16. Its subcontainers are quality profiles.
  17. """
  18. def __init__(self, container_id: str, variant: "VariantNode", *, container: ContainerInterface = None) -> None:
  19. super().__init__(container_id)
  20. self.variant = variant
  21. self.qualities = {} # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
  22. self.materialChanged = Signal() # Triggered when the material is removed or its metadata is updated.
  23. container_registry = ContainerRegistry.getInstance()
  24. if container is not None:
  25. self.base_file = container.getMetaDataEntry("base_file")
  26. self.material_type = container.getMetaDataEntry("material")
  27. self.brand = container.getMetaDataEntry("brand")
  28. self.guid = container.getMetaDataEntry("GUID")
  29. else:
  30. my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
  31. self.base_file = my_metadata["base_file"]
  32. self.material_type = my_metadata["material"]
  33. self.brand = my_metadata["brand"]
  34. self.guid = my_metadata["GUID"]
  35. self._loadAll()
  36. container_registry.containerRemoved.connect(self._onRemoved)
  37. container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
  38. def preferredQuality(self) -> QualityNode:
  39. """Finds the preferred quality for this printer with this material and this variant loaded.
  40. If the preferred quality is not available, an arbitrary quality is returned. If there is a configuration
  41. mistake (like a typo in the preferred quality) this returns a random available quality. If there are no
  42. available qualities, this will return the empty quality node.
  43. :return: The node for the preferred quality, or any arbitrary quality if there is no match.
  44. """
  45. for quality_id, quality_node in self.qualities.items():
  46. if self.variant.machine.preferred_quality_type == quality_node.quality_type:
  47. return quality_node
  48. fallback = next(iter(self.qualities.values())) # Should only happen with empty quality node.
  49. Logger.log("w", "Could not find preferred quality type {preferred_quality_type} for material {material_id} and variant {variant_id}, falling back to {fallback}.".format(
  50. preferred_quality_type = self.variant.machine.preferred_quality_type,
  51. material_id = self.container_id,
  52. variant_id = self.variant.container_id,
  53. fallback = fallback.container_id
  54. ))
  55. return fallback
  56. @UM.FlameProfiler.profile
  57. def _loadAll(self) -> None:
  58. container_registry = ContainerRegistry.getInstance()
  59. # Find all quality profiles that fit on this material.
  60. if not self.variant.machine.has_machine_quality: # Need to find the global qualities.
  61. qualities = container_registry.findInstanceContainersMetadata(type = "quality",
  62. definition = "fdmprinter")
  63. elif not self.variant.machine.has_materials:
  64. qualities = container_registry.findInstanceContainersMetadata(type = "quality",
  65. definition = self.variant.machine.quality_definition)
  66. else:
  67. if self.variant.machine.has_variants:
  68. # Need to find the qualities that specify a material profile with the same material type.
  69. qualities = container_registry.findInstanceContainersMetadata(type = "quality",
  70. definition = self.variant.machine.quality_definition,
  71. variant = self.variant.variant_name,
  72. material = self.base_file) # First try by exact material ID.
  73. # CURA-7070
  74. # The quality profiles only reference a material with the material_root_id. They will never state something
  75. # such as "generic_pla_ultimaker_s5_AA_0.4". So we search with the "base_file" which is the material_root_id.
  76. else:
  77. qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, material = self.base_file)
  78. if not qualities:
  79. my_material_type = self.material_type
  80. if self.variant.machine.has_variants:
  81. qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality",
  82. definition = self.variant.machine.quality_definition,
  83. variant = self.variant.variant_name)
  84. else:
  85. qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition)
  86. # First we attempt to find materials that have the same brand but not the right color
  87. all_material_base_files_right_brand = {material_metadata["base_file"] for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type, brand = self.brand)}
  88. right_brand_no_color_qualities = [quality for quality in qualities_any_material if quality.get("material") in all_material_base_files_right_brand]
  89. if right_brand_no_color_qualities:
  90. # We found qualties for materials with the right brand but not with the right color. Use those.
  91. qualities.extend(right_brand_no_color_qualities)
  92. else:
  93. # Fall back to generic
  94. all_material_base_files = {material_metadata["base_file"] for material_metadata in
  95. container_registry.findInstanceContainersMetadata(type="material",
  96. material=my_material_type)}
  97. no_brand_no_color_qualities = (quality for quality in qualities_any_material if
  98. quality.get("material") in all_material_base_files)
  99. qualities.extend(no_brand_no_color_qualities)
  100. if not qualities: # No quality profiles found. Go by GUID then.
  101. my_guid = self.guid
  102. for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
  103. qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["base_file"]))
  104. if not qualities:
  105. # There are still some machines that should use global profiles in the extruder, so do that now.
  106. # These are mostly older machines that haven't received updates (so single extruder machines without specific qualities
  107. # but that do have materials and profiles specific to that machine)
  108. qualities.extend([quality for quality in qualities_any_material if quality.get("global_quality", "False") != "False"])
  109. for quality in qualities:
  110. quality_id = quality["id"]
  111. if quality_id not in self.qualities:
  112. self.qualities[quality_id] = QualityNode(quality_id, parent = self)
  113. if not self.qualities:
  114. self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self)
  115. def _onRemoved(self, container: ContainerInterface) -> None:
  116. """Triggered when any container is removed, but only handles it when the container is removed that this node
  117. represents.
  118. :param container: The container that was allegedly removed.
  119. """
  120. if container.getId() == self.container_id:
  121. # Remove myself from my parent.
  122. if self.base_file in self.variant.materials:
  123. del self.variant.materials[self.base_file]
  124. if not self.variant.materials:
  125. self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant)
  126. self.materialChanged.emit(self)
  127. def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None:
  128. """Triggered when any metadata changed in any container, but only handles it when the metadata of this node is
  129. changed.
  130. :param container: The container whose metadata changed.
  131. :param kwargs: Key-word arguments provided when changing the metadata. These are ignored. As far as I know they
  132. are never provided to this call.
  133. """
  134. if container.getId() != self.container_id:
  135. return
  136. new_metadata = container.getMetaData()
  137. old_base_file = self.base_file
  138. if new_metadata["base_file"] != old_base_file:
  139. self.base_file = new_metadata["base_file"]
  140. if old_base_file in self.variant.materials: # Move in parent node.
  141. del self.variant.materials[old_base_file]
  142. self.variant.materials[self.base_file] = self
  143. old_material_type = self.material_type
  144. self.material_type = new_metadata["material"]
  145. old_guid = self.guid
  146. self.guid = new_metadata["GUID"]
  147. if self.base_file != old_base_file or self.material_type != old_material_type or self.guid != old_guid: # List of quality profiles could've changed.
  148. self.qualities = {}
  149. self._loadAll() # Re-load the quality profiles for this node.
  150. self.materialChanged.emit(self)