123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- # Copyright (c) 2019 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- from typing import TYPE_CHECKING
- from UM.Logger import Logger
- from UM.Settings.ContainerRegistry import ContainerRegistry
- from UM.Settings.Interfaces import ContainerInterface
- from UM.Signal import Signal
- from cura.Machines.ContainerNode import ContainerNode
- from cura.Machines.MaterialNode import MaterialNode
- import UM.FlameProfiler
- if TYPE_CHECKING:
- from typing import Dict
- from cura.Machines.MachineNode import MachineNode
- class VariantNode(ContainerNode):
- """This class represents an extruder variant in the container tree.
- The subnodes of these nodes are materials.
- This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific
- to one global stack, so because the list of materials can be different per stack depending on the compatible
- material diameter setting, we cannot filter them here. Filtering must be done in the model.
- """
- def __init__(self, container_id: str, machine: "MachineNode") -> None:
- super().__init__(container_id)
- self.machine = machine
- self.materials = {} # type: Dict[str, MaterialNode] # Mapping material base files to their nodes.
- self.materialsChanged = Signal()
- container_registry = ContainerRegistry.getInstance()
- self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"] # Store our own name so that we can filter more easily.
- container_registry.containerAdded.connect(self._materialAdded)
- container_registry.containerRemoved.connect(self._materialRemoved)
- self._loadAll()
- @UM.FlameProfiler.profile
- def _loadAll(self) -> None:
- """(Re)loads all materials under this variant."""
- container_registry = ContainerRegistry.getInstance()
- if not self.machine.has_materials:
- self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
- return # There should not be any materials loaded for this printer.
- # Find all the materials for this variant's name.
- else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
- base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
- printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id)
- 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.
- materials_per_base_file = {material["base_file"]: material for material in base_materials}
- materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
- materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials}) # Variant-specific profiles override all of those.
- materials = list(materials_per_base_file.values())
- # Filter materials based on the exclude_materials property.
- filtered_materials = [material for material in materials if not self.machine.isExcludedMaterialBaseFile(material["id"])]
- for material in filtered_materials:
- if material.get("abstract_color", False) and not self.machine.supports_abstract_color:
- continue # do not show abstract color profiles if the machine does not support them
- base_file = material["base_file"]
- if base_file not in self.materials:
- self.materials[base_file] = MaterialNode(material["id"], variant = self)
- self.materials[base_file].materialChanged.connect(self.materialsChanged)
- if not self.materials:
- self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
- def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
- """Finds the preferred material for this printer with this nozzle in one of the extruders.
- If the preferred material is not available, an arbitrary material is returned. If there is a configuration
- mistake (like a typo in the preferred material) this returns a random available material. If there are no
- available materials, this will return the empty material node.
- :param approximate_diameter: The desired approximate diameter of the material.
- :return: The node for the preferred material, or any arbitrary material if there is no match.
- """
- for base_material, material_node in self.materials.items():
- if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
- return material_node
- # First fallback: Check if we should be checking for the 175 variant.
- if approximate_diameter == 2:
- preferred_material = self.machine.preferred_material + "_175"
- for base_material, material_node in self.materials.items():
- if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
- return material_node
- # Second fallback: Choose any material with matching diameter.
- for material_node in self.materials.values():
- if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
- Logger.log("w", "Could not find preferred material %s, falling back to whatever works", self.machine.preferred_material)
- return material_node
- fallback = next(iter(self.materials.values())) # Should only happen with empty material node.
- Logger.log("w", "Could not find preferred material {preferred_material} with diameter {diameter} for variant {variant_id}, falling back to {fallback}.".format(
- preferred_material = self.machine.preferred_material,
- diameter = approximate_diameter,
- variant_id = self.container_id,
- fallback = fallback.container_id
- ))
- return fallback
- @UM.FlameProfiler.profile
- def _materialAdded(self, container: ContainerInterface) -> None:
- """When a material gets added to the set of profiles, we need to update our tree here."""
- if container.getMetaDataEntry("type") != "material":
- return # Not interested.
- if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):
- # CURA-6889
- # containerAdded and removed signals may be triggered in the next event cycle. If a container gets added
- # and removed in the same event cycle, in the next cycle, the connections should just ignore the signals.
- # The check here makes sure that the container in the signal still exists.
- Logger.log("d", "Got container added signal for container [%s] but it no longer exists, do nothing.",
- container.getId())
- return
- if not self.machine.has_materials:
- return # We won't add any materials.
- material_definition = container.getMetaDataEntry("definition")
- base_file = container.getMetaDataEntry("base_file")
- if self.machine.isExcludedMaterialBaseFile(base_file):
- return # Material is forbidden for this printer.
- 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.
- if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
- return
- material_variant = container.getMetaDataEntry("variant_name")
- if material_variant is not None and material_variant != self.variant_name:
- return
- else: # We already have this base profile. Replace the base profile if the new one is more specific.
- new_definition = container.getMetaDataEntry("definition")
- if new_definition == "fdmprinter":
- return # Just as unspecific or worse.
- material_variant = container.getMetaDataEntry("variant_name")
- if new_definition != self.machine.container_id or material_variant != self.variant_name:
- return # Doesn't match this set-up.
- original_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.materials[base_file].container_id)[0]
- if "variant_name" in original_metadata or material_variant is None:
- return # Original was already specific or just as unspecific as the new one.
- if "empty_material" in self.materials:
- del self.materials["empty_material"]
- self.materials[base_file] = MaterialNode(container.getId(), variant = self, container = container)
- self.materials[base_file].materialChanged.connect(self.materialsChanged)
- self.materialsChanged.emit(self.materials[base_file])
- @UM.FlameProfiler.profile
- def _materialRemoved(self, container: ContainerInterface) -> None:
- if container.getMetaDataEntry("type") != "material":
- return # Only interested in materials.
- base_file = container.getMetaDataEntry("base_file")
- if base_file not in self.materials:
- return # We don't track this material anyway. No need to remove it.
- original_node = self.materials[base_file]
- del self.materials[base_file]
- self.materialsChanged.emit(original_node)
- # Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
- # Search for any submaterials from that base file that are still left.
- materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
- if materials_same_base_file:
- most_specific_submaterial = None
- for submaterial in materials_same_base_file:
- if submaterial["definition"] == self.machine.container_id:
- if submaterial.get("variant_name", "empty") == self.variant_name:
- most_specific_submaterial = submaterial
- break # most specific match possible
- if submaterial.get("variant_name", "empty") == "empty":
- most_specific_submaterial = submaterial
- if most_specific_submaterial is None:
- Logger.log("w", "Material %s removed, but no suitable replacement found", base_file)
- else:
- Logger.log("i", "Material %s (%s) overridden by %s", base_file, self.variant_name, most_specific_submaterial.get("id"))
- self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
- self.materialsChanged.emit(self.materials[base_file])
- if not self.materials: # The last available material just got deleted and there is nothing with the same base file to replace it.
- self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
- self.materialsChanged.emit(self.materials["empty_material"])
|