VariantNode.py 11 KB

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