VariantNode.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import 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.MaterialNode import MaterialNode
  10. import UM.FlameProfiler
  11. if TYPE_CHECKING:
  12. from typing import Dict
  13. from cura.Machines.MachineNode import MachineNode
  14. class VariantNode(ContainerNode):
  15. """This class represents an extruder variant in the container tree.
  16. The subnodes of these nodes are materials.
  17. This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific
  18. to one global stack, so because the list of materials can be different per stack depending on the compatible
  19. material diameter setting, we cannot filter them here. Filtering must be done in the model.
  20. """
  21. def __init__(self, container_id: str, machine: "MachineNode") -> None:
  22. super().__init__(container_id)
  23. self.machine = machine
  24. self.materials = {} # type: Dict[str, MaterialNode] # Mapping material base files to their nodes.
  25. self.materialsChanged = Signal()
  26. container_registry = ContainerRegistry.getInstance()
  27. self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"] # Store our own name so that we can filter more easily.
  28. container_registry.containerAdded.connect(self._materialAdded)
  29. container_registry.containerRemoved.connect(self._materialRemoved)
  30. self._loadAll()
  31. @UM.FlameProfiler.profile
  32. def _loadAll(self) -> None:
  33. """(Re)loads all materials under this variant."""
  34. container_registry = ContainerRegistry.getInstance()
  35. if not self.machine.has_materials:
  36. self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
  37. return # There should not be any materials loaded for this printer.
  38. # Find all the materials for this variant's name.
  39. else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
  40. base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
  41. printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id)
  42. variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything.
  43. materials_per_base_file = {material["base_file"]: material for material in base_materials}
  44. materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
  45. materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials}) # Variant-specific profiles override all of those.
  46. materials = list(materials_per_base_file.values())
  47. # Filter materials based on the exclude_materials property.
  48. filtered_materials = [material for material in materials if not self.machine.isExcludedMaterialBaseFile(material["id"])]
  49. for material in filtered_materials:
  50. if material.get("abstract_color", False) and not self.machine.supports_abstract_color:
  51. continue # do not show abstract color profiles if the machine does not support them
  52. base_file = material["base_file"]
  53. if base_file not in self.materials:
  54. self.materials[base_file] = MaterialNode(material["id"], variant = self)
  55. self.materials[base_file].materialChanged.connect(self.materialsChanged)
  56. if not self.materials:
  57. self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
  58. def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
  59. """Finds the preferred material for this printer with this nozzle in one of the extruders.
  60. If the preferred material is not available, an arbitrary material is returned. If there is a configuration
  61. mistake (like a typo in the preferred material) this returns a random available material. If there are no
  62. available materials, this will return the empty material node.
  63. :param approximate_diameter: The desired approximate diameter of the material.
  64. :return: The node for the preferred material, or any arbitrary material if there is no match.
  65. """
  66. for base_material, material_node in self.materials.items():
  67. if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
  68. return material_node
  69. # First fallback: Check if we should be checking for the 175 variant.
  70. if approximate_diameter == 2:
  71. preferred_material = self.machine.preferred_material + "_175"
  72. for base_material, material_node in self.materials.items():
  73. if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
  74. return material_node
  75. # Second fallback: Choose any material with matching diameter.
  76. for material_node in self.materials.values():
  77. if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
  78. Logger.log("w", "Could not find preferred material %s, falling back to whatever works", self.machine.preferred_material)
  79. return material_node
  80. fallback = next(iter(self.materials.values())) # Should only happen with empty material node.
  81. Logger.log("w", "Could not find preferred material {preferred_material} with diameter {diameter} for variant {variant_id}, falling back to {fallback}.".format(
  82. preferred_material = self.machine.preferred_material,
  83. diameter = approximate_diameter,
  84. variant_id = self.container_id,
  85. fallback = fallback.container_id
  86. ))
  87. return fallback
  88. @UM.FlameProfiler.profile
  89. def _materialAdded(self, container: ContainerInterface) -> None:
  90. """When a material gets added to the set of profiles, we need to update our tree here."""
  91. if container.getMetaDataEntry("type") != "material":
  92. return # Not interested.
  93. if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):
  94. # CURA-6889
  95. # containerAdded and removed signals may be triggered in the next event cycle. If a container gets added
  96. # and removed in the same event cycle, in the next cycle, the connections should just ignore the signals.
  97. # The check here makes sure that the container in the signal still exists.
  98. Logger.log("d", "Got container added signal for container [%s] but it no longer exists, do nothing.",
  99. container.getId())
  100. return
  101. if not self.machine.has_materials:
  102. return # We won't add any materials.
  103. material_definition = container.getMetaDataEntry("definition")
  104. base_file = container.getMetaDataEntry("base_file")
  105. if self.machine.isExcludedMaterialBaseFile(base_file):
  106. return # Material is forbidden for this printer.
  107. if base_file not in self.materials: # Completely new base file. Always better than not having a file as long as it matches our set-up.
  108. if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
  109. return
  110. material_variant = container.getMetaDataEntry("variant_name")
  111. if material_variant is not None and material_variant != self.variant_name:
  112. return
  113. else: # We already have this base profile. Replace the base profile if the new one is more specific.
  114. new_definition = container.getMetaDataEntry("definition")
  115. if new_definition == "fdmprinter":
  116. return # Just as unspecific or worse.
  117. material_variant = container.getMetaDataEntry("variant_name")
  118. if new_definition != self.machine.container_id or material_variant != self.variant_name:
  119. return # Doesn't match this set-up.
  120. original_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.materials[base_file].container_id)[0]
  121. if "variant_name" in original_metadata or material_variant is None:
  122. return # Original was already specific or just as unspecific as the new one.
  123. if "empty_material" in self.materials:
  124. del self.materials["empty_material"]
  125. self.materials[base_file] = MaterialNode(container.getId(), variant = self, container = container)
  126. self.materials[base_file].materialChanged.connect(self.materialsChanged)
  127. self.materialsChanged.emit(self.materials[base_file])
  128. @UM.FlameProfiler.profile
  129. def _materialRemoved(self, container: ContainerInterface) -> None:
  130. if container.getMetaDataEntry("type") != "material":
  131. return # Only interested in materials.
  132. base_file = container.getMetaDataEntry("base_file")
  133. if base_file not in self.materials:
  134. return # We don't track this material anyway. No need to remove it.
  135. original_node = self.materials[base_file]
  136. del self.materials[base_file]
  137. self.materialsChanged.emit(original_node)
  138. # Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
  139. # Search for any submaterials from that base file that are still left.
  140. materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
  141. if materials_same_base_file:
  142. most_specific_submaterial = None
  143. for submaterial in materials_same_base_file:
  144. if submaterial["definition"] == self.machine.container_id:
  145. if submaterial.get("variant_name", "empty") == self.variant_name:
  146. most_specific_submaterial = submaterial
  147. break # most specific match possible
  148. if submaterial.get("variant_name", "empty") == "empty":
  149. most_specific_submaterial = submaterial
  150. if most_specific_submaterial is None:
  151. Logger.log("w", "Material %s removed, but no suitable replacement found", base_file)
  152. else:
  153. Logger.log("i", "Material %s (%s) overridden by %s", base_file, self.variant_name, most_specific_submaterial.get("id"))
  154. self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
  155. self.materialsChanged.emit(self.materials[base_file])
  156. if not self.materials: # The last available material just got deleted and there is nothing with the same base file to replace it.
  157. self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
  158. self.materialsChanged.emit(self.materials["empty_material"])