Merge branch 'feature_intent_container_tree' of https://github.com/Ultimaker/Cura into feature_intent_container_tree

This commit is contained in:
Remco Burema 2019-08-20 13:53:12 +02:00
commit 5039d8db05
22 changed files with 258 additions and 398 deletions

View File

@ -5,10 +5,15 @@ from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added. from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added.
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.MachineNode import MachineNode from cura.Machines.MachineNode import MachineNode
from typing import Dict from typing import Dict, List, TYPE_CHECKING
import time import time
if TYPE_CHECKING:
from cura.Machines.QualityGroup import QualityGroup
## This class contains a look-up tree for which containers are available at ## This class contains a look-up tree for which containers are available at
# which stages of configuration. # which stages of configuration.
# #
@ -29,6 +34,21 @@ class ContainerTree:
container_registry.containerAdded.connect(self._machineAdded) container_registry.containerAdded.connect(self._machineAdded)
self._loadAll() self._loadAll()
## Get the quality groups available for the currently activated printer.
#
# This contains all quality groups, enabled or disabled. To check whether
# the quality group can be activated, test for the
# ``QualityGroup.is_available`` property.
# \return For every quality type, one quality group.
def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return {}
variant_names = [extruder.variant.getName() for extruder in global_stack.extruders.values()]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruders.values()]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruders.values()]
return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
## Builds the initial container tree. ## Builds the initial container tree.
def _loadAll(self): def _loadAll(self):
Logger.log("i", "Building container tree.") Logger.log("i", "Building container tree.")

View File

@ -1,19 +1,16 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import Dict, List
from UM.Logger import Logger from UM.Logger import Logger
from UM.Util import parseBool from UM.Util import parseBool
from UM.Settings.ContainerRegistry import ContainerRegistry # To find all the variants for this machine. from UM.Settings.ContainerRegistry import ContainerRegistry # To find all the variants for this machine.
from UM.Settings.Interfaces import ContainerInterface
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.QualityGroup import QualityGroup # To construct groups of quality profiles that belong together.
from cura.Machines.QualityNode import QualityNode from cura.Machines.QualityNode import QualityNode
from cura.Machines.VariantNode import VariantNode from cura.Machines.VariantNode import VariantNode
if TYPE_CHECKING:
from typing import Dict
## This class represents a machine in the container tree. ## This class represents a machine in the container tree.
# #
# The subnodes of these nodes are variants. # The subnodes of these nodes are variants.
@ -36,10 +33,57 @@ class MachineNode(ContainerNode):
self.quality_definition = my_metadata.get("quality_definition", container_id) self.quality_definition = my_metadata.get("quality_definition", container_id)
self.exclude_materials = my_metadata.get("exclude_materials", []) self.exclude_materials = my_metadata.get("exclude_materials", [])
self.preferred_variant_name = my_metadata.get("preferred_variant_name", "") self.preferred_variant_name = my_metadata.get("preferred_variant_name", "")
self.preferred_quality_type = my_metadata.get("preferred_quality_type", "")
container_registry.containerAdded.connect(self._variantAdded)
self._loadAll() self._loadAll()
## Get the available quality groups for this machine.
#
# This returns all quality groups, regardless of whether they are
# available to the combination of extruders or not. On the resulting
# quality groups, the is_available property is set to indicate whether the
# quality group can be selected according to the combination of extruders
# in the parameters.
# \param variant_names The names of the variants loaded in each extruder.
# \param material_bases The base file names of the materials loaded in
# each extruder.
# \param extruder_enabled Whether or not the extruders are enabled. This
# allows the function to set the is_available properly.
# \return For each available quality type, a QualityGroup instance.
def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]:
if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled):
Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").")
return {}
# For each extruder, find which quality profiles are available. Later we'll intersect the quality types.
qualities_per_type_per_extruder = [{} for _ in range(len(variant_names))] # type: List[Dict[str, QualityNode]]
for extruder_nr, variant_name in enumerate(variant_names):
if not extruder_enabled[extruder_nr]:
continue # No qualities are available in this extruder. It'll get skipped when calculating the available quality types.
material_base = material_bases[extruder_nr]
if variant_name not in self.variants or material_base not in self.variants[variant_name].materials:
# The printer has no variant/material-specific quality profiles. Use the global quality profiles.
qualities_per_type_per_extruder[extruder_nr] = self.global_qualities
else:
# Use the actually specialised quality profiles.
qualities_per_type_per_extruder[extruder_nr] = self.variants[variant_name].materials[material_base].qualities
# Create the quality group for each available type.
quality_groups = {}
for quality_type, global_quality_node in self.global_qualities.items():
quality_groups[quality_type] = QualityGroup(name = global_quality_node.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
quality_groups[quality_type].node_for_global = global_quality_node
for extruder, qualities_per_type in qualities_per_type_per_extruder:
quality_groups[quality_type].nodes_for_extruders[extruder] = qualities_per_type[quality_type]
available_quality_types = set(quality_groups.keys())
for extruder_nr, qualities_per_type in enumerate(qualities_per_type_per_extruder):
if not extruder_enabled[extruder_nr]:
continue
available_quality_types.intersection_update(qualities_per_type.keys())
for quality_type in available_quality_types:
quality_groups[quality_type].is_available = True
return quality_groups
## (Re)loads all variants under this printer. ## (Re)loads all variants under this printer.
def _loadAll(self): def _loadAll(self):
# Find all the variants for this definition ID. # Find all the variants for this definition ID.
@ -55,19 +99,4 @@ class MachineNode(ContainerNode):
if len(global_qualities) == 0: # This printer doesn't override the global qualities. if len(global_qualities) == 0: # This printer doesn't override the global qualities.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = True) # Otherwise pick the global global qualities. global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = True) # Otherwise pick the global global qualities.
for global_quality in global_qualities: for global_quality in global_qualities:
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self) self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)
## When a variant gets added to the set of profiles, we need to update our
# tree here.
def _variantAdded(self, container: ContainerInterface):
if container.getMetaDataEntry("type") != "variant":
return # Not interested.
name = container.getMetaDataEntry("name")
if name in self.variants:
return # Already have this one.
if container.getMetaDataEntry("hardware_type") != "nozzle":
return # Only want nozzles in my tree.
if container.getMetaDataEntry("definition") != self.container_id:
return # Not a nozzle that fits in my machine.
self.variants[name] = VariantNode(container.getId(), machine = self)

View File

@ -59,15 +59,6 @@ class MaterialManager(QObject):
# Root_material_id -> MaterialGroup # Root_material_id -> MaterialGroup
self._material_group_map = dict() # type: Dict[str, MaterialGroup] self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Approximate diameter str
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
# We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175
# root_material_id -> approximate diameter str -> root_material_id for that diameter
self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
# Material id including diameter (generic_pla_175) -> material root id (generic_pla) # Material id including diameter (generic_pla_175) -> material root id (generic_pla)
self._diameter_material_map = dict() # type: Dict[str, str] self._diameter_material_map = dict() # type: Dict[str, str]
@ -75,11 +66,6 @@ class MaterialManager(QObject):
# GUID -> a list of material_groups # GUID -> a list of material_groups
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
# The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found.
self._default_machine_definition_id = "fdmprinter"
self._default_approximate_diameter_for_quality_search = "3"
self._favorites = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";")) self._favorites = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
self.materialsUpdated.emit() self.materialsUpdated.emit()
@ -87,7 +73,15 @@ class MaterialManager(QObject):
return self._material_group_map.get(root_material_id) return self._material_group_map.get(root_material_id)
def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) original_material = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(id=root_material_id)[0]
if original_material["approximate_diameter"] == approximate_diameter:
return root_material_id
matching_materials = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", brand = original_material["brand"], definition = original_material["definition"], material = original_material["material"], color_name = original_material["color_name"])
for material in matching_materials:
if material["approximate_diameter"] == approximate_diameter:
return material["id"]
return root_material_id
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id, "") return self._diameter_material_map.get(root_material_id, "")
@ -109,13 +103,16 @@ class MaterialManager(QObject):
# A convenience function to get available materials for the given machine with the extruder position. # A convenience function to get available materials for the given machine with the extruder position.
# #
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]: extruder_stack: "ExtruderStack") -> Dict[str, MaterialNode]:
nozzle_name = None nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant": if extruder_stack.variant.getId() != "empty_variant":
nozzle_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup. # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine.definition.getId(), nozzle_name) materials =self.getAvailableMaterials(machine.definition.getId(), nozzle_name)
compatible_material_diameter = str(round(extruder_stack.getCompatibleMaterialDiameter()))
return {key: material for key, material in materials.items() if material.getMetaDataEntry("approximate_diameter") == compatible_material_diameter}
# #
# Gets MaterialNode for the given extruder and machine with the given material name. # Gets MaterialNode for the given extruder and machine with the given material name.
@ -152,27 +149,10 @@ class MaterialManager(QObject):
# #
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
node = None
machine_definition = global_stack.definition machine_definition = global_stack.definition
extruder_definition = global_stack.extruders[position].definition variant_name = global_stack.extruders[position].variant.getName()
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
material_diameter = extruder_definition.getProperty("material_diameter", "value")
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(global_stack)
# Look at the guid to material dictionary return self.getMaterialNode(machine_definition.getId(), variant_name, buildplate_name, 3, material_guid)
root_material_id = None
for material_group in self._guid_material_groups_map[material_guid]:
root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
break
if not root_material_id:
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
return None
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id)
return node
# There are 2 ways to get fallback materials; # There are 2 ways to get fallback materials;
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material # - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
@ -254,41 +234,21 @@ class MaterialManager(QObject):
return node return node
def removeMaterialByRootId(self, root_material_id: str): def removeMaterialByRootId(self, root_material_id: str):
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
return
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list results = container_registry.findContainers(id=root_material_id)
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container if not results:
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted. container_registry.addWrongContainerId(root_material_id)
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container for result in results:
# list, so removeContainer() can ignore those ones. container_registry.removeContainer(result.getMetaDataEntry("id", ""))
for node in nodes_to_remove:
container_id = node.getMetaDataEntry("id", "")
results = container_registry.findContainers(id = container_id)
if not results:
container_registry.addWrongContainerId(container_id)
for node in nodes_to_remove:
container_registry.removeContainer(node.getMetaDataEntry("id", ""))
#
# Methods for GUI
#
@pyqtSlot("QVariant", result=bool) @pyqtSlot("QVariant", result=bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"): def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed! # Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it # In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration) # corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id) ids_to_remove = [metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file=root_material_id)]
if not material_group:
return False
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "extruder_train"): for extruder_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "extruder_train"):
if extruder_stack.material.getId() in ids_to_remove: if extruder_stack.material.getId() in ids_to_remove:
@ -303,38 +263,24 @@ class MaterialManager(QObject):
if CuraContainerRegistry.getInstance().isReadOnly(root_material_id): if CuraContainerRegistry.getInstance().isReadOnly(root_material_id):
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
return return
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = root_material_id)
containers[0].setName(name)
material_group = self.getMaterialGroup(root_material_id)
if material_group:
container = material_group.root_material_node.container
if container:
container.setName(name)
#
# Removes the given material.
#
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode") -> None: def removeMaterial(self, material_node: "MaterialNode") -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is not None: if root_material_id is not None:
self.removeMaterialByRootId(root_material_id) self.removeMaterialByRootId(root_material_id)
# def duplicateMaterialByRootId(self, root_material_id, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
# Creates a duplicate of a material, which has the same GUID and base_file metadata. container_registry = CuraContainerRegistry.getInstance()
# Returns the root material ID of the duplicated material if successful. results = container_registry.findContainers(id=root_material_id)
#
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
material_group = self.getMaterialGroup(root_material_id) if not results:
if not material_group:
Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
return None return None
base_container = material_group.root_material_node.container base_container = results[0]
if not base_container:
return None
# Ensure all settings are saved. # Ensure all settings are saved.
cura.CuraApplication.CuraApplication.getInstance().saveSettings() cura.CuraApplication.CuraApplication.getInstance().saveSettings()
@ -353,11 +299,9 @@ class MaterialManager(QObject):
new_containers.append(new_base_container) new_containers.append(new_base_container)
# Clone all of them. # Clone all of them.
for node in material_group.derived_material_node_list: for container_to_copy in container_registry.findContainers(base_file=root_material_id):
container_to_copy = node.container if container_to_copy.getId() == root_material_id:
if not container_to_copy: continue # We already have that one, skip it
continue
# Create unique IDs for every clone.
new_id = new_base_id new_id = new_base_id
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition") new_id += "_" + container_to_copy.getMetaDataEntry("definition")
@ -371,7 +315,6 @@ class MaterialManager(QObject):
if new_metadata is not None: if new_metadata is not None:
for key, value in new_metadata.items(): for key, value in new_metadata.items():
new_container.getMetaData()[key] = value new_container.getMetaData()[key] = value
new_containers.append(new_container) new_containers.append(new_container)
for container_to_add in new_containers: for container_to_add in new_containers:
@ -383,8 +326,15 @@ class MaterialManager(QObject):
self.addFavorite(new_base_id) self.addFavorite(new_base_id)
return new_base_id return new_base_id
# #
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
# Returns the root material ID of the duplicated material if successful.
#
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
return self.duplicateMaterialByRootId(root_material_id, new_base_id, new_metadata)
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
# Returns the ID of the newly created material. # Returns the ID of the newly created material.
@pyqtSlot(result = str) @pyqtSlot(result = str)
@ -403,11 +353,6 @@ class MaterialManager(QObject):
approximate_diameter = str(extruder_stack.approximateMaterialDiameter) approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
material_group = self.getMaterialGroup(root_material_id)
if not material_group: # This should never happen
Logger.log("w", "Cannot get the material group of %s.", root_material_id)
return ""
# Create a new ID & container to hold the data. # Create a new ID & container to hold the data.
new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material") new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
@ -416,9 +361,7 @@ class MaterialManager(QObject):
"GUID": str(uuid.uuid4()), "GUID": str(uuid.uuid4()),
} }
self.duplicateMaterial(material_group.root_material_node, self.duplicateMaterialByRootId(root_material_id, new_base_id = new_id, new_metadata = new_metadata)
new_base_id = new_id,
new_metadata = new_metadata)
return new_id return new_id
@pyqtSlot(str) @pyqtSlot(str)

View File

@ -1,7 +1,7 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import Any, TYPE_CHECKING
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
@ -23,8 +23,11 @@ class MaterialNode(ContainerNode):
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
my_metadata = container_registry.findContainersMetadata(id = container_id)[0] my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
self.base_file = my_metadata["base_file"] self.base_file = my_metadata["base_file"]
container_registry.containerAdded.connect(self._qualityAdded) self.material_type = my_metadata["material"]
self.guid = my_metadata["GUID"]
self._loadAll() self._loadAll()
container_registry.containerRemoved.connect(self._onRemoved)
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
def _loadAll(self) -> None: def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
@ -33,14 +36,13 @@ class MaterialNode(ContainerNode):
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter") qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter")
else: else:
# Need to find the qualities that specify a material profile with the same material type. # Need to find the qualities that specify a material profile with the same material type.
my_metadata = container_registry.findInstanceContainersMetadata(id = self.container_id)[0] my_material_type = self.material_type
my_material_type = my_metadata.get("material")
qualities = [] qualities = []
qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name) qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name)
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type): for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"])) qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
if not qualities: # No quality profiles found. Go by GUID then. if not qualities: # No quality profiles found. Go by GUID then.
my_guid = my_metadata.get("material") my_guid = self.guid
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid): for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"])) qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
@ -49,37 +51,37 @@ class MaterialNode(ContainerNode):
if quality_id not in self.qualities: if quality_id not in self.qualities:
self.qualities[quality_id] = QualityNode(quality_id, parent = self) self.qualities[quality_id] = QualityNode(quality_id, parent = self)
def _qualityAdded(self, container: ContainerInterface) -> None: ## Triggered when any container is removed, but only handles it when the
if container.getMetaDataEntry("type") != "quality": # container is removed that this node represents.
return # Not interested. # \param container The container that was allegedly removed.
if not self.variant.machine.has_machine_quality: def _onRemoved(self, container: ContainerInterface) -> None:
if container.getMetaDataEntry("definition") != "fdmprinter": if container.getId() == self.container_id:
return # Only want global qualities. # Remove myself from my parent.
else: if self.base_file in self.variant.materials:
if container.getMetaDataEntry("definition") != self.variant.machine.quality_definition: del self.variant.materials[self.base_file]
return # Doesn't match the machine.
if container.getMetaDataEntry("variant") != self.variant.variant_name:
return # Doesn't match the variant.
# Detect if we're falling back to matching via GUID.
# If so, we might need to erase the current list and put just this one in (i.e. no longer use the fallback).
container_registry = ContainerRegistry.getInstance()
my_metadata = container_registry.findInstanceContainersMetadata(id = self.container_id)[0]
my_material_type = my_metadata.get("material")
allowed_material_ids = {metadata["id"] for metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type)}
# Select any quality profile; if the material is not matching by material type, we've been falling back to GUID all along.
is_fallback_guid = len(self.qualities) == 0 or next(iter(self.qualities.values())).getMetaDataEntry("material") not in allowed_material_ids
if is_fallback_guid and container.getMetaDataEntry("material") in allowed_material_ids: # So far we needed the fallback, but no longer! ## Triggered when any metadata changed in any container, but only handles
self.qualities.clear() # It'll get filled with the new quality profile then. # it when the metadata of this node is changed.
else: # \param container The container whose metadata changed.
if not is_fallback_guid: # \param kwargs Key-word arguments provided when changing the metadata.
if container.getMetaDataEntry("material") not in allowed_material_ids: # These are ignored. As far as I know they are never provided to this
return # Doesn't match the material type. # call.
else: def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None:
my_material_guid = my_metadata.get("GUID") if container.getId() != self.container_id:
allowed_material_ids = {metadata["id"] for metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_material_guid)} return
if container.getMetaDataEntry("material") not in allowed_material_ids:
return # Doesn't match the material GUID.
quality_id = container.getId() new_metadata = container.getMetaData()
self.qualities[quality_id] = QualityNode(quality_id, parent = self) old_base_file = self.base_file
if new_metadata["base_file"] != old_base_file:
self.base_file = new_metadata["base_file"]
if old_base_file in self.variant.materials: # Move in parent node.
del self.variant.materials[old_base_file]
self.variant.materials[self.base_file] = self
old_material_type = self.material_type
self.material_type = new_metadata["material"]
old_guid = self.guid
self.guid = new_metadata["GUID"]
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.
self.qualities = {}
self._loadAll() # Re-load the quality profiles for this node.

View File

@ -34,9 +34,6 @@ class BaseMaterialsModel(ListModel):
# Update this model when switching machines # Update this model when switching machines
self._machine_manager.activeStackChanged.connect(self._update) self._machine_manager.activeStackChanged.connect(self._update)
# Update this model when list of materials changes
self._material_manager.materialsUpdated.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id") self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id") self.addRoleName(Qt.UserRole + 2, "id")
@ -109,26 +106,24 @@ class BaseMaterialsModel(ListModel):
# so it's placed here for easy access. # so it's placed here for easy access.
def _canUpdate(self): def _canUpdate(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if global_stack is None or not self._enabled: if global_stack is None or not self._enabled:
return False return False
extruder_position = str(self._extruder_position) extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders: if extruder_position not in global_stack.extruders:
return False return False
extruder_stack = global_stack.extruders[extruder_position] extruder_stack = global_stack.extruders[extruder_position]
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None:
return False
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
return True return True
## This is another convenience function which is shared by all material ## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code. # models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node): def _createMaterialItem(self, root_material_id, container_node):
metadata = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id)[0] metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id)
if not metadata_list:
return None
metadata = metadata_list[0]
item = { item = {
"root_material_id": root_material_id, "root_material_id": root_material_id,
"id": metadata["id"], "id": metadata["id"],

View File

@ -2,11 +2,20 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
import cura.CuraApplication # To listen to changes to the preferences.
## Model that shows the list of favorite materials. ## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel): class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
self._update()
## Triggered when any preference changes, but only handles it when the list
# of favourites is changed.
def _onFavoritesChanged(self, preference_key: str) -> None:
if preference_key != "cura/favorite_materials":
return
self._update() self._update()
def _update(self): def _update(self):
@ -28,7 +37,8 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
continue continue
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item) if item:
item_list.append(item)
# Sort the item list alphabetically by name # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["brand"].upper()) item_list = sorted(item_list, key = lambda d: d["brand"].upper())

View File

@ -24,11 +24,12 @@ class GenericMaterialsModel(BaseMaterialsModel):
continue continue
# Only add results for generic materials # Only add results for generic materials
if container_node.getMetaDataEntry("brand").lower() != "generic": if container_node.getMetaDataEntry("brand", "unknown").lower() != "generic":
continue continue
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item) if item:
item_list.append(item)
# Sort the item list alphabetically by name # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["name"].upper()) item_list = sorted(item_list, key = lambda d: d["name"].upper())

View File

@ -8,6 +8,7 @@ from PyQt5.QtCore import Qt, QObject, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.IntentManager import IntentManager from cura.Settings.IntentManager import IntentManager
import cura.CuraApplication import cura.CuraApplication
@ -47,13 +48,11 @@ class IntentModel(ListModel):
def _update(self) -> None: def _update(self) -> None:
new_items = [] # type: List[Dict[str, Any]] new_items = [] # type: List[Dict[str, Any]]
application = cura.CuraApplication.CuraApplication.getInstance() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
quality_manager = application.getQualityManager()
global_stack = application.getGlobalContainerStack()
if not global_stack: if not global_stack:
self.setItems(new_items) self.setItems(new_items)
return return
quality_groups = quality_manager.getQualityGroups(global_stack) quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
for intent_category, quality_type in IntentManager.getInstance().getCurrentAvailableIntents(): for intent_category, quality_type in IntentManager.getInstance().getCurrentAvailableIntents():
if intent_category == self._intent_category: if intent_category == self._intent_category:

View File

@ -55,7 +55,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Now handle the individual materials # Now handle the individual materials
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
brand_group_dict[brand][material_type].append(item) if item:
brand_group_dict[brand][material_type].append(item)
# Part 2: Organize the tree into models # Part 2: Organize the tree into models
# #

View File

@ -1,10 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import Qt, pyqtSlot
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from cura.Machines.ContainerTree import ContainerTree
# #
# This the QML model for the quality management page. # This the QML model for the quality management page.
@ -42,7 +43,7 @@ class QualityManagementModel(ListModel):
self.setItems([]) self.setItems([])
return return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack) quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack) quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items() available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()

View File

@ -105,37 +105,12 @@ class QualityManager(QObject):
# \return A dictionary with quality types as keys and the quality groups # \return A dictionary with quality types as keys and the quality groups
# for those types as values. # for those types as values.
def getQualityGroups(self, global_stack: "GlobalStack") -> Dict[str, QualityGroup]: def getQualityGroups(self, global_stack: "GlobalStack") -> Dict[str, QualityGroup]:
# Gather up the variant names and material base files for each extruder.
variant_names = [extruder.variant.getName() for extruder in global_stack.extruders.values()]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruders.values()]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruders.values()]
definition_id = global_stack.definition.getId() definition_id = global_stack.definition.getId()
machine_node = ContainerTree.getInstance().machines[definition_id] return ContainerTree.getInstance().machines[definition_id].getQualityGroups(variant_names, material_bases, extruder_enabled)
# For each extruder, find which quality profiles are available. Later we'll intersect the quality types.
qualities_per_type_per_extruder = {} # type: Dict[str, Dict[str, QualityNode]]
for extruder_nr, extruder in global_stack.extruders.items():
if not extruder.isEnabled:
continue # No qualities available in this extruder. It'll get skipped when intersecting the quality types.
nozzle_name = extruder.variant.getName()
material_base = extruder.material.getMetaDataEntry("base_file")
if nozzle_name not in machine_node.variants or material_base not in machine_node.variants[nozzle_name].materials:
# The printer has no variant/material-specific quality profiles. Use the global quality profiles.
qualities_per_type_per_extruder[extruder_nr] = machine_node.global_qualities
else:
# Use the actually specialised quality profiles.
qualities_per_type_per_extruder[extruder_nr] = machine_node.variants[nozzle_name].materials[material_base].qualities
# Create the quality group for each available type.
quality_groups = {}
for quality_type, global_quality_node in machine_node.global_qualities.items():
quality_groups[quality_type] = QualityGroup(name = global_quality_node.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
quality_groups[quality_type].node_for_global = global_quality_node
for extruder, qualities_per_type in qualities_per_type_per_extruder:
quality_groups[quality_type].nodes_for_extruders[extruder] = qualities_per_type[quality_type]
available_quality_types = set(quality_groups.keys())
for qualities_per_type in qualities_per_type_per_extruder.values():
available_quality_types.intersection_update(qualities_per_type.keys())
for quality_type in available_quality_types:
quality_groups[quality_type].is_available = True
return quality_groups
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]: def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
@ -160,11 +135,22 @@ class QualityManager(QObject):
return quality_group_dict return quality_group_dict
## Get the quality group for the preferred quality type for a certain
# global stack.
#
# If the preferred quality type is not available, ``None`` will be
# returned.
# \param machine The global stack of the machine to get the preferred
# quality group for.
# \return The preferred quality group, or ``None`` if that is not
# available.
def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]: def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
preferred_quality_type = machine.definition.getMetaDataEntry("preferred_quality_type") machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
quality_group_dict = self.getQualityGroups(machine) quality_groups = self.getQualityGroups(machine)
quality_group = quality_group_dict.get(preferred_quality_type) result = quality_groups.get(machine_node.preferred_quality_type)
return quality_group if result is not None and result.is_available:
return result
return None # If preferred quality type is not available, leave it up for the caller.
# #

View File

@ -4,7 +4,6 @@
from typing import Union, TYPE_CHECKING from typing import Union, TYPE_CHECKING
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.IntentNode import IntentNode from cura.Machines.IntentNode import IntentNode
@ -21,7 +20,6 @@ class QualityNode(ContainerNode):
super().__init__(container_id) super().__init__(container_id)
self.parent = parent self.parent = parent
self.intents = {} # type: Dict[str, IntentNode] self.intents = {} # type: Dict[str, IntentNode]
ContainerRegistry.getInstance().containerAdded.connect(self._intentAdded)
self._loadAll() self._loadAll()
def _loadAll(self) -> None: def _loadAll(self) -> None:
@ -31,21 +29,4 @@ class QualityNode(ContainerNode):
if not isinstance(self.parent, MachineNode): # Not a global profile. if not isinstance(self.parent, MachineNode): # Not a global profile.
for intent in container_registry.findInstanceContainersMetadata(type = "intent", definition = self.parent.variant.machine.quality_definition, variant = self.parent.variant.variant_name, material = self.parent.base_file): for intent in container_registry.findInstanceContainersMetadata(type = "intent", definition = self.parent.variant.machine.quality_definition, variant = self.parent.variant.variant_name, material = self.parent.base_file):
self.intents[intent["id"]] = IntentNode(intent["id"], quality = self) self.intents[intent["id"]] = IntentNode(intent["id"], quality = self)
# Otherwise, there are no intents for global profiles. # Otherwise, there are no intents for global profiles.
def _intentAdded(self, container: ContainerInterface) -> None:
from cura.Machines.MachineNode import MachineNode # Imported here to prevent circular imports.
if container.getMetaDataEntry("type") != "intent":
return # Not interested if it's not an intent.
if isinstance(self.parent, MachineNode):
return # Global profiles don't have intents.
if container.getMetaDataEntry("definition") != self.parent.variant.machine.quality_definition:
return # Incorrect printer.
if container.getMetaDataEntry("variant") != self.parent.variant.variant_name:
return # Incorrect variant.
if container.getMetaDataEntry("material") != self.parent.base_file:
return # Incorrect material.
container_id = container.getId()
if container_id in self.intents:
return # Already have this.
self.intents[container_id] = IntentNode(container_id, quality = self)

View File

@ -115,7 +115,7 @@ class VariantManager:
variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]: variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]:
if variant_type is None: if variant_type is None:
variant_node = None variant_node = None
variant_type_dict = self._machine_to_variant_dict_map[machine_definition_id] variant_type_dict = self._machine_to_variant_dict_map.get("machine_definition_id", {})
for variant_dict in variant_type_dict.values(): for variant_dict in variant_type_dict.values():
if variant_name in variant_dict: if variant_name in variant_dict:
variant_node = variant_dict[variant_name] variant_node = variant_dict[variant_name]

View File

@ -9,7 +9,6 @@ from typing import Dict, Union, Any, TYPE_CHECKING, List
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
@ -17,18 +16,19 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from UM.Platform import Platform from UM.Platform import Platform
from UM.SaveFile import SaveFile from UM.SaveFile import SaveFile
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication import cura.CuraApplication
from cura.Machines.MaterialManager import MaterialManager
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup from cura.Machines.QualityChangesGroup import QualityChangesGroup
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager from cura.Machines.QualityManager import QualityManager
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -323,22 +323,18 @@ class ContainerManager(QObject):
## Get a list of materials that have the same GUID as the reference material ## Get a list of materials that have the same GUID as the reference material
# #
# \param material_id \type{str} the id of the material for which to get the linked materials. # \param material_node The node representing the material for which to get
# \return \type{list} a list of names of materials with the same GUID # the same GUID.
# \param exclude_self Whether to include the name of the material you
# provided.
# \return A list of names of materials with the same GUID.
@pyqtSlot("QVariant", bool, result = "QStringList") @pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False): def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
guid = material_node.getMetaDataEntry("GUID", "") same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(guid = material_node.guid)
if exclude_self:
self_root_material_id = material_node.getMetaDataEntry("base_file") return [metadata["name"] for metadata in same_guid if metadata["base_file"] != material_node.base_file]
material_group_list = MaterialManager.getInstance().getMaterialGroupListByGUID(guid) else:
return [metadata["name"] for metadata in same_guid]
linked_material_names = []
if material_group_list:
for material_group in material_group_list:
if exclude_self and material_group.name == self_root_material_id:
continue
linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", ""))
return linked_material_names
## Unlink a material from all other materials by creating a new GUID ## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for. # \param material_id \type{str} the id of the material to create a new GUID for.

View File

@ -28,7 +28,6 @@ class CuraStackBuilder:
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
quality_manager = application.getQualityManager()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
definitions = registry.findDefinitionContainers(id = definition_id) definitions = registry.findDefinitionContainers(id = definition_id)
@ -64,7 +63,7 @@ class CuraStackBuilder:
registry.addContainer(new_extruder) registry.addContainer(new_extruder)
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type") preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(new_global_stack) quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
if not quality_group_dict: if not quality_group_dict:
# There is no available quality group, set all quality containers to empty. # There is no available quality group, set all quality containers to empty.
new_global_stack.quality = application.empty_quality_container new_global_stack.quality = application.empty_quality_container

View File

@ -4,6 +4,7 @@
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
import cura.CuraApplication import cura.CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container from cura.Settings.cura_empty_instance_containers import empty_intent_container
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
@ -75,7 +76,8 @@ class IntentManager(QObject):
# TODO: We now do this (return a default) if the global stack is missing, but not in the code below, # TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
# even though there should always be defaults. The problem then is what to do with the quality_types. # even though there should always be defaults. The problem then is what to do with the quality_types.
# Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default. # Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default.
quality_groups = application.getQualityManager().getQualityGroups(global_stack) quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
# TODO: These quality nodes in that tree already contain the intent nodes. We can optimise this.
available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None} available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}
final_intent_ids = set() # type: Set[str] final_intent_ids = set() # type: Set[str]

View File

@ -1,8 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional
from cura.CuraApplication import CuraApplication from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
@ -24,32 +25,27 @@ class ClusterPrinterConfigurationMaterial(BaseModel):
self.material = material self.material = material
super().__init__(**kwargs) super().__init__(**kwargs)
## Creates a material output model based on this cloud printer material. ## Creates a material output model based on this cloud printer material.
#
# A material is chosen that matches the current GUID. If multiple such
# materials are available, read-only materials are preferred and the
# material with the earliest alphabetical name will be selected.
# \return A material output model that matches the current GUID.
def createOutputModel(self) -> MaterialOutputModel: def createOutputModel(self) -> MaterialOutputModel:
material_manager = CuraApplication.getInstance().getMaterialManager() container_registry = ContainerRegistry.getInstance()
material_group_list = material_manager.getMaterialGroupListByGUID(self.guid) or [] same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid)
if same_guid:
# Sort the material groups by "is_read_only = True" first, and then the name alphabetically. read_only = sorted(filter(lambda metadata: container_registry.isReadOnly(metadata["id"]), same_guid), key = lambda metadata: metadata["name"])
read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list)) if read_only:
non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list)) material_metadata = read_only[0]
material_group = None else:
if read_only_material_group_list: material_metadata = min(same_guid, key = lambda metadata: metadata["name"])
read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
material_group = read_only_material_group_list[0]
elif non_read_only_material_group_list:
non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
material_group = non_read_only_material_group_list[0]
if material_group:
container = material_group.root_material_node.container
color = container.getMetaDataEntry("color_code")
brand = container.getMetaDataEntry("brand")
material_type = container.getMetaDataEntry("material")
name = container.getName()
else: else:
color = self.color material_metadata = {
brand = self.brand "color_code": self.color,
material_type = self.material "brand": self.brand,
name = "Empty" if self.material == "empty" else "Unknown" "material": self.material,
"name": "Empty" if self.material == "empty" else "Unknown"
}
return MaterialOutputModel(guid=self.guid, type=material_type, brand=brand, color=color, name=name) return MaterialOutputModel(guid = self.guid, type = material_metadata["material"], brand = material_metadata["brand"], color = material_metadata["color_code"], name = material_metadata["name"])

View File

@ -244,7 +244,10 @@ class XmlMaterialProfile(InstanceContainer):
variant_name = container.getMetaDataEntry("variant_name") variant_name = container.getMetaDataEntry("variant_name")
if variant_name: if variant_name:
variant_dict = {"variant_node": variant_manager.getVariantNode(definition_id, variant_name), variant_node = variant_manager.getVariantNode(definition_id, variant_name)
if variant_node is None:
continue
variant_dict = {"variant_node":variant_node ,
"material_container": container} "material_container": container}
machine_variant_map[definition_id][variant_name] = variant_dict machine_variant_map[definition_id][variant_name] = variant_dict
continue continue

View File

@ -17,6 +17,8 @@ Item
property var resetEnabled: false property var resetEnabled: false
property var currentItem: null property var currentItem: null
property var materialManager: CuraApplication.getMaterialManager()
property var hasCurrentItem: base.currentItem != null property var hasCurrentItem: base.currentItem != null
property var isCurrentItemActivated: property var isCurrentItemActivated:
{ {
@ -119,7 +121,7 @@ Item
onClicked: onClicked:
{ {
forceActiveFocus(); forceActiveFocus();
base.newRootMaterialIdToSwitchTo = CuraApplication.getMaterialManager().createMaterial(); base.newRootMaterialIdToSwitchTo = base.materialManager.createMaterial();
base.toActivateNewMaterial = true; base.toActivateNewMaterial = true;
} }
} }
@ -134,7 +136,7 @@ Item
onClicked: onClicked:
{ {
forceActiveFocus(); forceActiveFocus();
base.newRootMaterialIdToSwitchTo = CuraApplication.getMaterialManager().duplicateMaterial(base.currentItem.container_node); base.newRootMaterialIdToSwitchTo = base.materialManager.duplicateMaterial(base.currentItem.container_node);
base.toActivateNewMaterial = true; base.toActivateNewMaterial = true;
} }
} }
@ -145,7 +147,8 @@ Item
id: removeMenuButton id: removeMenuButton
text: catalog.i18nc("@action:button", "Remove") text: catalog.i18nc("@action:button", "Remove")
iconName: "list-remove" iconName: "list-remove"
enabled: base.hasCurrentItem && !base.currentItem.is_read_only && !base.isCurrentItemActivated && CuraApplication.getMaterialManager().canMaterialBeRemoved(base.currentItem.container_node) enabled: base.hasCurrentItem && !base.currentItem.is_read_only && !base.isCurrentItemActivated && base.materialManager.canMaterialBeRemoved(base.currentItem.container_node)
onClicked: onClicked:
{ {
forceActiveFocus(); forceActiveFocus();
@ -294,7 +297,7 @@ Item
{ {
// Set the active material as the fallback. It will be selected when the current material is deleted // Set the active material as the fallback. It will be selected when the current material is deleted
base.newRootMaterialIdToSwitchTo = base.active_root_material_id base.newRootMaterialIdToSwitchTo = base.active_root_material_id
CuraApplication.getMaterialManager().removeMaterial(base.currentItem.container_node); base.materialManager.removeMaterial(base.currentItem.container_node);
} }
} }

View File

@ -4,14 +4,6 @@ import pytest
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
from cura.Machines.MachineNode import MachineNode from cura.Machines.MachineNode import MachineNode
machine_node_variant_added_test_data = [({"type": "Not a variant!"}, ["Variant One", "Variant Two"]), # Wrong type
({"type": "variant", "name": "Variant One"}, ["Variant One", "Variant Two"]), # Name already added
({"type": "variant", "name": "Variant Three", "hardware_type": "Not a nozzle"}, ["Variant One", "Variant Two"]), # Wrong hardware type
({"type": "variant", "name": "Variant Three", "hardware_type": "nozzle", "definition": "machine_3"}, ["Variant One", "Variant Two"]), # Wrong definition ID
({"type": "variant", "name": "Variant Three", "hardware_type": "nozzle", "definition": "machine_1"}, ["Variant One", "Variant Two", "Variant Three"])] # Yay! It's finally added
metadata_dict = {} metadata_dict = {}
@ -44,18 +36,4 @@ def test_machineNodeInit(container_registry):
# As variants get stored by name, we want to check if those get added. # As variants get stored by name, we want to check if those get added.
assert "Variant One" in machine_node.variants assert "Variant One" in machine_node.variants
assert "Variant Two" in machine_node.variants assert "Variant Two" in machine_node.variants
assert len(machine_node.variants) == 2 # And ensure that *only* those two got added. assert len(machine_node.variants) == 2 # And ensure that *only* those two got added.
@pytest.mark.parametrize("metadata,variant_result_list", machine_node_variant_added_test_data)
def test_machineNodeVariantAdded(container_registry, metadata, variant_result_list):
machine_node = createMachineNode("machine_1", container_registry)
with patch("cura.Machines.MachineNode.VariantNode"): # We're not testing the variant node here, so patch it out.
with patch.dict(metadata_dict, metadata):
mocked_container = createMockedInstanceContainer()
machine_node._variantAdded(mocked_container)
assert len(variant_result_list) == len(machine_node.variants)
for name in variant_result_list:
assert name in machine_node.variants

View File

@ -6,21 +6,6 @@ from cura.Machines.MaterialNode import MaterialNode
instance_container_metadata_dict = {"fdmprinter": {"no_variant": [{"id": "quality_1", "material": "material_1"}]}, instance_container_metadata_dict = {"fdmprinter": {"no_variant": [{"id": "quality_1", "material": "material_1"}]},
"machine_1": {"variant_1": {"material_1": [{"id": "quality_2", "material": "material_1"}, {"id": "quality_3","material": "material_1"}]}}} "machine_1": {"variant_1": {"material_1": [{"id": "quality_2", "material": "material_1"}, {"id": "quality_3","material": "material_1"}]}}}
quality_metadata_machine_quality_test_data = [({"type": "Not a quality"}, ["quality_2", "quality_3"]), # Wrong type
({"type": "quality", "definition": "machine_2"}, ["quality_2", "quality_3"]), # Wrong defintion
({"type": "quality", "definition": "machine_1", "variant": "variant_2"}, ["quality_2", "quality_3"]), # Wrong variant
({"type": "quality", "definition": "machine_1", "variant": "variant_1", "material": "material_2"}, ["quality_2", "quality_3"]), # wrong material
]
quality_metadata_no_machine_quality =[({"type": "Not a quality"}, ["quality_1"]), # Wrong type
({"type": "quality", "definition": "machine_1"}, ["quality_1"]), # Wrong defintion (it needs fdmprinter)
({"type": "quality", "definition": "fdmprinter", "variant": "variant_2"}, ["quality_1", "quality_4"]), # Wrong variant, but should be added (as we ignore the variant)
({"type": "quality", "definition": "fdmprinter", "variant": "variant_1", "material": "material_2"}, ["quality_1", "quality_4"]), # wrong material, but should be added (as we ignore the material)
({"type": "quality", "definition": "fdmprinter", "variant": "variant_1", "material": "material_1"}, ["quality_1", "quality_4"]),
]
metadata_dict = {} metadata_dict = {}
@ -86,45 +71,4 @@ def test_materialNodeInit_MachineQuality(container_registry):
assert len(node.qualities) == 2 assert len(node.qualities) == 2
assert "quality_2" in node.qualities assert "quality_2" in node.qualities
assert "quality_3" in node.qualities assert "quality_3" in node.qualities
@pytest.mark.parametrize("metadata,qualities_result_list", quality_metadata_machine_quality_test_data)
def test_qualityAdded_hasMachineQuality(container_registry, metadata, qualities_result_list):
variant_node = MagicMock()
variant_node.variant_name = "variant_1"
variant_node.machine.has_machine_quality = True
variant_node.machine.quality_definition = "machine_1"
container = createMockedInstanceContainer("quality_4")
with patch("cura.Machines.MaterialNode.QualityNode"):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
node = MaterialNode("material_1", variant_node)
with patch.dict(metadata_dict, metadata):
node._qualityAdded(container)
assert len(qualities_result_list) == len(node.qualities)
for name in qualities_result_list:
assert name in node.qualities
@pytest.mark.parametrize("metadata,qualities_result_list", quality_metadata_no_machine_quality)
def test_qualityAdded_noMachineQuality(container_registry, metadata, qualities_result_list):
variant_node = MagicMock()
variant_node.variant_name = "variant_1"
variant_node.machine.has_machine_quality = False
container = createMockedInstanceContainer("quality_4")
with patch("cura.Machines.MaterialNode.QualityNode"):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
node = MaterialNode("material_1", variant_node)
with patch.dict(metadata_dict, metadata):
node._qualityAdded(container)
assert len(qualities_result_list) == len(node.qualities)
for name in qualities_result_list:
assert name in node.qualities

View File

@ -1,21 +1,12 @@
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import pytest import pytest
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityNode import QualityNode from cura.Machines.QualityNode import QualityNode
instance_container_metadata_dict = {"fdmprinter": {"variant_1": {"material_1": [{"id": "intent_1"}, {"id": "intent_2"}]}}, instance_container_metadata_dict = {"fdmprinter": {"variant_1": {"material_1": [{"id": "intent_1"}, {"id": "intent_2"}]}},
"machine_1": {"variant_2": {"material_2": [{"id": "intent_3"}, {"id": "intent_4"}]}}} "machine_1": {"variant_2": {"material_2": [{"id": "intent_3"}, {"id": "intent_4"}]}}}
intent_metadata_intent_added_data = [({"type": "Not an intent"}, ["intent_3", "intent_4"]), # Wrong type
({"type": "intent", "definition": "machine_9000"}, ["intent_3", "intent_4"]), # wrong definition
({"type": "intent", "definition": "machine_1", "variant": "variant_299101"}, ["intent_3", "intent_4"]), # wrong variant
({"type": "intent", "definition": "machine_1", "variant": "variant_2", "material": "super cool material!"}, ["intent_3", "intent_4"]), # Wrong material
({"type": "intent", "definition": "machine_1", "variant": "variant_2", "material": "material_2"}, ["intent_3", "intent_4", "intent_9001"]), # Yay, all good.
]
metadata_dict = {} metadata_dict = {}
@ -55,24 +46,4 @@ def test_qualityNode_machine_1(container_registry):
assert len(node.intents) == 2 assert len(node.intents) == 2
assert "intent_3" in node.intents assert "intent_3" in node.intents
assert "intent_4" in node.intents assert "intent_4" in node.intents
@pytest.mark.parametrize("metadata,intent_result_list", intent_metadata_intent_added_data)
def test_intentNodeAdded(container_registry, metadata, intent_result_list):
material_node = MagicMock()
material_node.variant.machine.quality_definition = "machine_1"
material_node.variant.variant_name = "variant_2"
material_node.base_file = "material_2"
intent_container = createMockedInstanceContainer("intent_9001")
with patch("cura.Machines.QualityNode.IntentNode"):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
node = QualityNode("quality_1", material_node)
with patch.dict(metadata_dict, metadata):
node._intentAdded(intent_container)
assert len(intent_result_list) == len(node.intents)
for identifier in intent_result_list:
assert identifier in node.intents