From 066a00653ab6ba62fcd5267a352646a7078af142 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 2 Aug 2018 17:15:30 +0200 Subject: [PATCH] Add one more layer to the decision tree --- cura/Machines/MaterialManager.py | 200 +++++++++++------- cura/Machines/QualityManager.py | 125 ++++++----- cura/Settings/GlobalStack.py | 6 + cura/Settings/MachineManager.py | 22 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 199 ++++++++++++----- resources/definitions/fdmprinter.def.json | 1 + 6 files changed, 368 insertions(+), 185 deletions(-) diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 5d691fcef4..86f7dea81f 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -4,8 +4,7 @@ from collections import defaultdict, OrderedDict import copy import uuid -from typing import Dict, cast -from typing import Optional, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot @@ -18,6 +17,7 @@ from UM.Util import parseBool from .MaterialNode import MaterialNode from .MaterialGroup import MaterialGroup +from .VariantType import VariantType if TYPE_CHECKING: from UM.Settings.DefinitionContainer import DefinitionContainer @@ -47,7 +47,7 @@ class MaterialManager(QObject): self._fallback_materials_map = dict() # material_type -> generic material metadata self._material_group_map = dict() # root_material_id -> MaterialGroup - self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode) + self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> 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 @@ -186,10 +186,11 @@ class MaterialManager(QObject): for root_material_id in data_dict.values(): self._diameter_material_map[root_material_id] = default_root_material_id + variant_manager = self._application.getVariantManager() + # Map #4 - # "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer - # Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer - self._diameter_machine_variant_material_map = dict() + # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer + self._diameter_machine_nozzle_buildplate_material_map = dict() for material_metadata in material_metadatas.values(): # We don't store empty material in the lookup tables if material_metadata["id"] == "empty_material": @@ -199,36 +200,62 @@ class MaterialManager(QObject): definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] - if approximate_diameter not in self._diameter_machine_variant_material_map: - self._diameter_machine_variant_material_map[approximate_diameter] = {} + if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: + self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} - machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter] - if definition not in machine_variant_material_map: - machine_variant_material_map[definition] = MaterialNode() + machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] + if definition not in machine_nozzle_buildplate_material_map: + machine_nozzle_buildplate_material_map[definition] = MaterialNode() - machine_node = machine_variant_material_map[definition] - variant_name = material_metadata.get("variant_name") - if not variant_name: - # if there is no variant, this material is for the machine, so put its metadata in the machine node. + machine_node = machine_nozzle_buildplate_material_map[definition] + nozzle_name = material_metadata.get("variant_name") + buildplate_name = material_metadata.get("buildplate_name") + if not nozzle_name: + # if there is no nozzle, this material is for the machine, so put its metadata in the machine node. machine_node.material_map[root_material_id] = MaterialNode(material_metadata) else: - # this material is variant-specific, so we save it in a variant-specific node under the + # this material is nozzle-specific, so we save it in a nozzle-specific node under the # machine-specific node - # Check first if the variant exist in the manager - existing_variant = self._application.getVariantManager().getVariantNode(definition, variant_name) - if existing_variant is not None: - if variant_name not in machine_node.children_map: - machine_node.children_map[variant_name] = MaterialNode() + # Check first if the nozzle exists in the manager + existing_nozzle = variant_manager.getVariantNode(definition, nozzle_name, VariantType.NOZZLE) + if existing_nozzle is not None: + if nozzle_name not in machine_node.children_map: + machine_node.children_map[nozzle_name] = MaterialNode() + + nozzle_node = machine_node.children_map[nozzle_name] + + # Check build plate node + if not buildplate_name: + # if there is no buildplate, this material is for the machine, so put its metadata in the machine node. + if root_material_id in nozzle_node.material_map: # We shouldn't have duplicated nozzle-specific materials for the same machine. + ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) + continue + nozzle_node.material_map[root_material_id] = MaterialNode(material_metadata) + else: + # this material is nozzle-and-buildplate-specific, so we save it in a buildplate-specific node + # under the machine-specific node + + # Check first if the buildplate exists in the manager + existing_buildplate = variant_manager.getVariantNode(definition, buildplate_name, VariantType.BUILD_PLATE) + if existing_buildplate is not None: + if buildplate_name not in nozzle_node.children_map: + nozzle_node.children_map[buildplate_name] = MaterialNode() + + buildplate_node = nozzle_node.children_map[buildplate_name] + if root_material_id in buildplate_node.material_map: # We shouldn't have duplicated nozzle-and-buildplate-specific materials for the same machine. + ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) + continue + buildplate_node.material_map[root_material_id] = MaterialNode(material_metadata) + + else: + # Add this container id to the wrong containers list in the registry + Logger.log("w", "Not adding {id} to the material manager because the buildplate does not exist.".format(id = material_metadata["id"])) + self._container_registry.addWrongContainerId(material_metadata["id"]) - variant_node = machine_node.children_map[variant_name] - if root_material_id in variant_node.material_map: # We shouldn't have duplicated variant-specific materials for the same machine. - ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) - continue - variant_node.material_map[root_material_id] = MaterialNode(material_metadata) else: # Add this container id to the wrong containers list in the registry - Logger.log("w", "Not adding {id} to the material manager because the variant does not exist.".format(id = material_metadata["id"])) + Logger.log("w", "Not adding {id} to the material manager because the nozzle does not exist.".format(id = material_metadata["id"])) self._container_registry.addWrongContainerId(material_metadata["id"]) self.materialsUpdated.emit() @@ -263,45 +290,52 @@ class MaterialManager(QObject): # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # - def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str], - diameter: float) -> Dict[str, MaterialNode]: + def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_nozzle_name: Optional[str], + buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) - if rounded_diameter not in self._diameter_machine_variant_material_map: + if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() - # If there are variant materials, get the variant material - machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] - machine_node = machine_variant_material_map.get(machine_definition_id) - default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id) - variant_node = None - if extruder_variant_name is not None and machine_node is not None: - variant_node = machine_node.getChildNode(extruder_variant_name) + # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material + machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] + machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) + default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) + nozzle_node = None + buildplate_node = None + if extruder_nozzle_name is not None and machine_node is not None: + nozzle_node = machine_node.getChildNode(extruder_nozzle_name) + # Get buildplate node if possible + if nozzle_node is not None and buildplate_name is not None: + buildplate_node = nozzle_node.getChildNode(buildplate_name) - nodes_to_check = [variant_node, machine_node, default_machine_node] + nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] # Fallback mechanism of finding materials: - # 1. variant-specific material - # 2. machine-specific material - # 3. generic material (for fdmprinter) + # 1. buildplate-specific material + # 2. nozzle-specific material + # 3. machine-specific material + # 4. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) - material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] - for node in nodes_to_check: - if node is not None: - # Only exclude the materials that are explicitly specified in the "exclude_materials" field. - # Do not exclude other materials that are of the same type. - for material_id, node in node.material_map.items(): - if material_id in machine_exclude_materials: - Logger.log("d", "Exclude material [%s] for machine [%s]", - material_id, machine_definition.getId()) - continue + material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] + for current_node in nodes_to_check: + if current_node is None: + continue - if material_id not in material_id_metadata_dict: - material_id_metadata_dict[material_id] = node + # Only exclude the materials that are explicitly specified in the "exclude_materials" field. + # Do not exclude other materials that are of the same type. + for material_id, node in current_node.material_map.items(): + if material_id in machine_exclude_materials: + Logger.log("d", "Exclude material [%s] for machine [%s]", + material_id, machine_definition.getId()) + continue + + if material_id not in material_id_metadata_dict: + material_id_metadata_dict[material_id] = node return material_id_metadata_dict @@ -310,13 +344,14 @@ class MaterialManager(QObject): # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", extruder_stack: "ExtruderStack") -> Optional[dict]: - variant_name = None + buildplate_name = machine.getBuildplateName() + nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": - variant_name = extruder_stack.variant.getName() + nozzle_name = extruder_stack.variant.getName() diameter = extruder_stack.approximateMaterialDiameter # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. - return self.getAvailableMaterials(machine.definition, variant_name, diameter) + return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. @@ -324,32 +359,36 @@ class MaterialManager(QObject): # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # - def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str], - diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: + def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], + buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) - if rounded_diameter not in self._diameter_machine_variant_material_map: + if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None - # If there are variant materials, get the variant material - machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] - machine_node = machine_variant_material_map.get(machine_definition_id) - variant_node = None + # If there are nozzle materials, get the nozzle-specific material + machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] + machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) + nozzle_node = None + buildplate_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: - machine_node = machine_variant_material_map.get(self._default_machine_definition_id) - if machine_node is not None and extruder_variant_name is not None: - variant_node = machine_node.getChildNode(extruder_variant_name) + machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) + if machine_node is not None and nozzle_name is not None: + nozzle_node = machine_node.getChildNode(nozzle_name) + if nozzle_node is not None and buildplate_name is not None: + buildplate_node = nozzle_node.getChildNode(buildplate_name) # Fallback mechanism of finding materials: - # 1. variant-specific material - # 2. machine-specific material - # 3. generic material (for fdmprinter) - nodes_to_check = [variant_node, machine_node, - machine_variant_material_map.get(self._default_machine_definition_id)] + # 1. buildplate-specific material + # 2. nozzle-specific material + # 3. machine-specific material + # 4. generic material (for fdmprinter) + nodes_to_check = [buildplate_node, nozzle_node, machine_node, + machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)] material_node = None for node in nodes_to_check: @@ -366,7 +405,8 @@ class MaterialManager(QObject): # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # - def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]: + def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, + buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition extruder_definition = global_stack.extruders[position].definition @@ -385,7 +425,7 @@ class MaterialManager(QObject): Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None - node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, + node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node @@ -413,13 +453,17 @@ class MaterialManager(QObject): else: return None - ## Get default material for given global stack, extruder position and extruder variant name + ## Get default material for given global stack, extruder position and extruder nozzle name # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) - def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, extruder_variant_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: + def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str], + extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: node = None + + buildplate_name = global_stack.getBuildplateName() machine_definition = global_stack.definition if extruder_definition is None: extruder_definition = global_stack.extruders[position].definition + if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): # At this point the extruder_definition is not None material_diameter = extruder_definition.getProperty("material_diameter", "value") @@ -428,7 +472,7 @@ class MaterialManager(QObject): approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) - node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, + node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node @@ -515,8 +559,8 @@ class MaterialManager(QObject): if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry("definition") if container_to_copy.getMetaDataEntry("variant_name"): - variant_name = container_to_copy.getMetaDataEntry("variant_name") - new_id += "_" + variant_name.replace(" ", "_") + nozzle_name = container_to_copy.getMetaDataEntry("variant_name") + new_id += "_" + nozzle_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index 82a11f9960..273f1ae11f 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -45,7 +45,7 @@ class QualityManager(QObject): self._empty_quality_container = self._application.empty_quality_container self._empty_quality_changes_container = self._application.empty_quality_changes_container - self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup + self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._default_machine_definition_id = "fdmprinter" @@ -64,10 +64,10 @@ class QualityManager(QObject): def initialize(self): # Initialize the lookup tree for quality profiles with following structure: - # -> -> - # -> + # -> -> -> + # -> - self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup + self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality") @@ -79,47 +79,58 @@ class QualityManager(QObject): quality_type = metadata["quality_type"] root_material_id = metadata.get("material") - variant_name = metadata.get("variant") + nozzle_name = metadata.get("variant") + buildplate_name = metadata.get("buildplate") is_global_quality = metadata.get("global_quality", False) - is_global_quality = is_global_quality or (root_material_id is None and variant_name is None) + is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None) # Sanity check: material+variant and is_global_quality cannot be present at the same time - if is_global_quality and (root_material_id or variant_name): + if is_global_quality and (root_material_id or nozzle_name): ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"]) continue - if definition_id not in self._machine_variant_material_quality_type_to_quality_dict: - self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode() - machine_node = cast(QualityNode, self._machine_variant_material_quality_type_to_quality_dict[definition_id]) + if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict: + self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode() + machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id]) if is_global_quality: # For global qualities, save data in the machine node machine_node.addQualityMetadata(quality_type, metadata) continue - if variant_name is not None: - # If variant_name is specified in the quality/quality_changes profile, check if material is specified, - # too. - if variant_name not in machine_node.children_map: - machine_node.children_map[variant_name] = QualityNode() - variant_node = cast(QualityNode, machine_node.children_map[variant_name]) + # Check if nozzle si specified + if nozzle_name is not None: + if nozzle_name not in machine_node.children_map: + machine_node.children_map[nozzle_name] = QualityNode() + nozzle_node = cast(QualityNode, machine_node.children_map[nozzle_name]) + + # Check if buildplate is specified + if buildplate_name is not None: + if buildplate_name not in nozzle_node.children_map: + nozzle_node.children_map[buildplate_name] = QualityNode() + buildplate_node = cast(QualityNode, nozzle_node.children_map[buildplate_name]) + + if root_material_id is None: + buildplate_node.addQualityMetadata(quality_type, metadata) + else: + if root_material_id not in buildplate_node.children_map: + buildplate_node.children_map[root_material_id] = QualityNode() + material_node = cast(QualityNode, buildplate_node.children_map[root_material_id]) + + material_node.addQualityMetadata(quality_type, metadata) - if root_material_id is None: - # If only variant_name is specified but material is not, add the quality/quality_changes metadata - # into the current variant node. - variant_node.addQualityMetadata(quality_type, metadata) else: - # If only variant_name and material are both specified, go one level deeper: create a material node - # under the current variant node, and then add the quality/quality_changes metadata into the - # material node. - if root_material_id not in variant_node.children_map: - variant_node.children_map[root_material_id] = QualityNode() - material_node = cast(QualityNode, variant_node.children_map[root_material_id]) + if root_material_id is None: + nozzle_node.addQualityMetadata(quality_type, metadata) + else: + if root_material_id not in nozzle_node.children_map: + nozzle_node.children_map[root_material_id] = QualityNode() + material_node = cast(QualityNode, nozzle_node.children_map[root_material_id]) - material_node.addQualityMetadata(quality_type, metadata) + material_node.addQualityMetadata(quality_type, metadata) else: - # If variant_name is not specified, check if material is specified. + # If nozzle is not specified, check if material is specified. if root_material_id is not None: if root_material_id not in machine_node.children_map: machine_node.children_map[root_material_id] = QualityNode() @@ -217,8 +228,8 @@ class QualityManager(QObject): # To find the quality container for the GlobalStack, check in the following fall-back manner: # (1) the machine-specific node # (2) the generic node - machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) - default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) + machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) + default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) nodes_to_check = [machine_node, default_machine_node] # Iterate over all quality_types in the machine node @@ -238,11 +249,13 @@ class QualityManager(QObject): quality_group_dict[quality_type] = quality_group break + buildplate_name = machine.getBuildplateName() + # Iterate over all extruders to find quality containers for each extruder for position, extruder in machine.extruders.items(): - variant_name = None + nozzle_name = None if extruder.variant.getId() != "empty_variant": - variant_name = extruder.variant.getName() + nozzle_name = extruder.variant.getName() # This is a list of root material IDs to use for searching for suitable quality profiles. # The root material IDs in this list are in prioritized order. @@ -258,34 +271,47 @@ class QualityManager(QObject): # Also try to get the fallback material material_type = extruder.material.getMetaDataEntry("material") fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type) - if fallback_root_material_id: + if fallback_root_material_id and root_material_id not in root_material_id_list: root_material_id_list.append(fallback_root_material_id) # Here we construct a list of nodes we want to look for qualities with the highest priority first. # The use case is that, when we look for qualities for a machine, we first want to search in the following # order: - # 1. machine-variant-and-material-specific qualities if exist - # 2. machine-variant-specific qualities if exist - # 3. machine-material-specific qualities if exist - # 4. machine-specific qualities if exist - # 5. generic qualities if exist + # 1. machine-nozzle-buildplate-and-material-specific qualities if exist + # 2. machine-nozzle-and-material-specific qualities if exist + # 3. machine-nozzle-specific qualities if exist + # 4. machine-material-specific qualities if exist + # 5. machine-specific qualities if exist + # 6. generic qualities if exist # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch # qualities from there. nodes_to_check = [] - if variant_name: - # In this case, we have both a specific variant and a specific material - variant_node = machine_node.getChildNode(variant_name) - if variant_node and has_material: + if nozzle_name: + # In this case, we have both a specific nozzle and a specific material + nozzle_node = machine_node.getChildNode(nozzle_name) + if nozzle_node and has_material: + # Check build plate if exists + if buildplate_name: + buildplate_node = nozzle_node.getChildNode(buildplate_name) + if buildplate_node and has_material: + for root_material_id in root_material_id_list: + material_node = buildplate_node.getChildNode(root_material_id) + if material_node: + nodes_to_check.append(material_node) + break + nodes_to_check.append(buildplate_node) + + # Then add nozzle specific materials for root_material_id in root_material_id_list: - material_node = variant_node.getChildNode(root_material_id) + material_node = nozzle_node.getChildNode(root_material_id) if material_node: nodes_to_check.append(material_node) break - nodes_to_check.append(variant_node) + nodes_to_check.append(nozzle_node) - # In this case, we only have a specific material but NOT a variant + # In this case, we only have a specific material but NOT a nozzle if has_material: for root_material_id in root_material_id_list: material_node = machine_node.getChildNode(root_material_id) @@ -309,8 +335,9 @@ class QualityManager(QObject): quality_group_dict[quality_type] = quality_group quality_group = quality_group_dict[quality_type] - quality_group.nodes_for_extruders[position] = quality_node - break + if position not in quality_group.nodes_for_extruders: + quality_group.nodes_for_extruders[position] = quality_node + #break # Update availabilities for each quality group self._updateQualityGroupsAvailability(machine, quality_group_dict.values()) @@ -323,8 +350,8 @@ class QualityManager(QObject): # To find the quality container for the GlobalStack, check in the following fall-back manner: # (1) the machine-specific node # (2) the generic node - machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) - default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get( + machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) + default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get( self._default_machine_definition_id) nodes_to_check = [machine_node, default_machine_node] diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index 66f3290b85..35edfe1053 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -55,6 +55,12 @@ class GlobalStack(CuraContainerStack): return "machine_stack" return configuration_type + def getBuildplateName(self) -> Optional[str]: + name = None + if self.variant.getId() != "empty_variant": + name = self.variant.getName() + return name + ## Add an extruder to the list of extruders of this stack. # # \param extruder The extruder to add. diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index ff585deb54..733340e0ce 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1256,13 +1256,17 @@ class MachineManager(QObject): else: position_list = [position] + buildplate_name = None + if self._global_container_stack.variant.getId() != "empty_variant": + buildplate_name = self._global_container_stack.variant.getName() + for position_item in position_list: extruder = self._global_container_stack.extruders[position_item] current_material_base_name = extruder.material.getMetaDataEntry("base_file") - current_variant_name = None + current_nozzle_name = None if extruder.variant.getId() != self._empty_variant_container.getId(): - current_variant_name = extruder.variant.getMetaDataEntry("name") + current_nozzle_name = extruder.variant.getMetaDataEntry("name") from UM.Settings.Interfaces import PropertyEvaluationContext from cura.Settings.CuraContainerStack import _ContainerIndexes @@ -1271,7 +1275,8 @@ class MachineManager(QObject): material_diameter = extruder.getProperty("material_diameter", "value", context) candidate_materials = self._material_manager.getAvailableMaterials( self._global_container_stack.definition, - current_variant_name, + current_nozzle_name, + buildplate_name, material_diameter) if not candidate_materials: @@ -1284,7 +1289,7 @@ class MachineManager(QObject): continue # The current material is not available, find the preferred one - material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_variant_name) + material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_nozzle_name) if material_node is not None: self._setMaterial(position_item, material_node) @@ -1389,12 +1394,17 @@ class MachineManager(QObject): def setMaterialById(self, position: str, root_material_id: str) -> None: if self._global_container_stack is None: return + buildplate_name = None + if self._global_container_stack.variant.getId() != "empty_variant": + buildplate_name = self._global_container_stack.variant.getName() + machine_definition_id = self._global_container_stack.definition.id position = str(position) extruder_stack = self._global_container_stack.extruders[position] - variant_name = extruder_stack.variant.getName() + nozzle_name = extruder_stack.variant.getName() material_diameter = extruder_stack.approximateMaterialDiameter - material_node = self._material_manager.getMaterialNode(machine_definition_id, variant_name, material_diameter, root_material_id) + material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name, + material_diameter, root_material_id) self.setMaterial(position, material_node) ## global_stack: if you want to provide your own global_stack instead of the current active one diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index a799f948c4..7d9b2aacc3 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -6,19 +6,17 @@ import io import json #To parse the product-to-id mapping file. import os.path #To find the product-to-id mapping. import sys -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, Tuple, cast import xml.etree.ElementTree as ET -from typing import Dict -from typing import Iterator from UM.Resources import Resources from UM.Logger import Logger -from cura.CuraApplication import CuraApplication import UM.Dictionary from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.ConfigurationErrorMessage import ConfigurationErrorMessage +from cura.CuraApplication import CuraApplication from cura.Machines.VariantType import VariantType from .XmlMaterialValidator import XmlMaterialValidator @@ -695,74 +693,38 @@ class XmlMaterialProfile(InstanceContainer): if buildplate_id is None: continue - from cura.Machines.VariantManager import VariantType variant_manager = CuraApplication.getInstance().getVariantManager() variant_node = variant_manager.getVariantNode(machine_id, buildplate_id, variant_type = VariantType.BUILD_PLATE) if not variant_node: continue - buildplate_compatibility = machine_compatibility - buildplate_recommended = machine_compatibility - settings = buildplate.iterfind("./um:setting", self.__namespaces) - for entry in settings: - key = entry.get("key") - if key in self.__unmapped_settings: - if key == "hardware compatible": - buildplate_compatibility = self._parseCompatibleValue(entry.text) - elif key == "hardware recommended": - buildplate_recommended = self._parseCompatibleValue(entry.text) - else: - Logger.log("d", "Unsupported material setting %s", key) + _, buildplate_unmapped_settings_dict = self._getSettingsDictForNode(buildplate) + + buildplate_compatibility = buildplate_unmapped_settings_dict.get("hardware compatible", + machine_compatibility) + buildplate_recommended = buildplate_unmapped_settings_dict.get("hardware recommended", + machine_compatibility) buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended hotends = machine.iterfind("./um:hotend", self.__namespaces) for hotend in hotends: - # The "id" field for hotends in material profiles are actually + # The "id" field for hotends in material profiles is actually name hotend_name = hotend.get("id") if hotend_name is None: continue variant_manager = CuraApplication.getInstance().getVariantManager() - variant_node = variant_manager.getVariantNode(machine_id, hotend_name) + variant_node = variant_manager.getVariantNode(machine_id, hotend_name, VariantType.NOZZLE) if not variant_node: continue - hotend_compatibility = machine_compatibility - hotend_setting_values = {} - settings = hotend.iterfind("./um:setting", self.__namespaces) - for entry in settings: - key = entry.get("key") - if key in self.__material_settings_setting_map: - if key == "processing temperature graph": #This setting has no setting text but subtags. - graph_nodes = entry.iterfind("./um:point", self.__namespaces) - graph_points = [] - for graph_node in graph_nodes: - flow = float(graph_node.get("flow")) - temperature = float(graph_node.get("temperature")) - graph_points.append([flow, temperature]) - hotend_setting_values[self.__material_settings_setting_map[key]] = str(graph_points) - else: - hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text - elif key in self.__unmapped_settings: - if key == "hardware compatible": - hotend_compatibility = self._parseCompatibleValue(entry.text) - else: - Logger.log("d", "Unsupported material setting %s", key) - - # Add namespaced Cura-specific settings - settings = hotend.iterfind("./cura:setting", self.__namespaces) - for entry in settings: - value = entry.text - if value.lower() == "yes": - value = True - elif value.lower() == "no": - value = False - key = entry.get("key") - hotend_setting_values[key] = value + hotend_mapped_settings, hotend_unmapped_settings = self._getSettingsDictForNode(hotend) + hotend_compatibility = hotend_unmapped_settings.get("hardware compatible", machine_compatibility) + # Generate container ID for the hotend-specific material container new_hotend_specific_material_id = self.getId() + "_" + machine_id + "_" + hotend_name.replace(" ", "_") # Same as machine compatibility, keep the derived material containers consistent with the parent material @@ -787,7 +749,7 @@ class XmlMaterialProfile(InstanceContainer): new_hotend_material.getMetaData()["buildplate_recommended"] = buildplate_map["buildplate_recommended"] cached_hotend_setting_properties = cached_machine_setting_properties.copy() - cached_hotend_setting_properties.update(hotend_setting_values) + cached_hotend_setting_properties.update(hotend_mapped_settings) new_hotend_material.setCachedValues(cached_hotend_setting_properties) @@ -796,6 +758,61 @@ class XmlMaterialProfile(InstanceContainer): if is_new_material: containers_to_add.append(new_hotend_material) + # + # Build plates in hotend + # + buildplates = hotend.iterfind("./um:buildplate", self.__namespaces) + for buildplate in buildplates: + # The "id" field for buildplate in material profiles is actually name + buildplate_name = buildplate.get("id") + if buildplate_name is None: + continue + + variant_manager = CuraApplication.getInstance().getVariantManager() + variant_node = variant_manager.getVariantNode(machine_id, buildplate_name, VariantType.BUILD_PLATE) + if not variant_node: + continue + + buildplate_mapped_settings, buildplate_unmapped_settings = self._getSettingsDictForNode(buildplate) + buildplate_compatibility = buildplate_unmapped_settings.get("hardware compatible", + buildplate_map["buildplate_compatible"]) + buildplate_recommended = buildplate_unmapped_settings.get("hardware recommended", + buildplate_map["buildplate_recommended"]) + + # Generate container ID for the hotend-and-buildplate-specific material container + new_hotend_and_buildplate_specific_material_id = new_hotend_specific_material_id + "_" + buildplate_name.replace(" ", "_") + + # Same as machine compatibility, keep the derived material containers consistent with the parent material + if ContainerRegistry.getInstance().isLoaded(new_hotend_and_buildplate_specific_material_id): + new_hotend_and_buildplate_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_and_buildplate_specific_material_id)[0] + is_new_material = False + else: + new_hotend_and_buildplate_material = XmlMaterialProfile(new_hotend_and_buildplate_specific_material_id) + is_new_material = True + + new_hotend_and_buildplate_material.setMetaData(copy.deepcopy(new_hotend_material.getMetaData())) + new_hotend_and_buildplate_material.getMetaData()["id"] = new_hotend_and_buildplate_specific_material_id + new_hotend_and_buildplate_material.getMetaData()["name"] = self.getName() + new_hotend_and_buildplate_material.getMetaData()["variant_name"] = hotend_name + new_hotend_and_buildplate_material.getMetaData()["buildplate_name"] = buildplate_name + new_hotend_and_buildplate_material.setDefinition(machine_id) + # Don't use setMetadata, as that overrides it for all materials with same base file + new_hotend_and_buildplate_material.getMetaData()["compatible"] = buildplate_compatibility + new_hotend_and_buildplate_material.getMetaData()["machine_manufacturer"] = machine_manufacturer + new_hotend_and_buildplate_material.getMetaData()["definition"] = machine_id + new_hotend_and_buildplate_material.getMetaData()["buildplate_compatible"] = buildplate_compatibility + new_hotend_and_buildplate_material.getMetaData()["buildplate_recommended"] = buildplate_recommended + + cached_hotend_and_buildplate_setting_properties = cached_hotend_setting_properties.copy() + cached_hotend_and_buildplate_setting_properties.update(buildplate_mapped_settings) + + new_hotend_and_buildplate_material.setCachedValues(cached_hotend_and_buildplate_setting_properties) + + new_hotend_and_buildplate_material._dirty = False + + if is_new_material: + containers_to_add.append(new_hotend_and_buildplate_material) + # there is only one ID for a machine. Once we have reached here, it means we have already found # a workable ID for that machine, so there is no need to continue break @@ -803,6 +820,54 @@ class XmlMaterialProfile(InstanceContainer): for container_to_add in containers_to_add: ContainerRegistry.getInstance().addContainer(container_to_add) + @classmethod + def _getSettingsDictForNode(cls, node) -> Tuple[dict, dict]: + node_mapped_settings_dict = dict() + node_unmapped_settings_dict = dict() + + # Fetch settings in the "um" namespace + um_settings = node.iterfind("./um:setting", cls.__namespaces) + for um_setting_entry in um_settings: + setting_key = um_setting_entry.get("key") + + # Mapped settings + if setting_key in cls.__material_settings_setting_map: + if setting_key == "processing temperature graph": # This setting has no setting text but subtags. + graph_nodes = um_setting_entry.iterfind("./um:point", cls.__namespaces) + graph_points = [] + for graph_node in graph_nodes: + flow = float(graph_node.get("flow")) + temperature = float(graph_node.get("temperature")) + graph_points.append([flow, temperature]) + node_mapped_settings_dict[cls.__material_settings_setting_map[setting_key]] = str( + graph_points) + else: + node_mapped_settings_dict[cls.__material_settings_setting_map[setting_key]] = um_setting_entry.text + + # Unmapped settings + elif setting_key in cls.__unmapped_settings: + if setting_key in ("hardware compatible", "hardware recommended"): + node_unmapped_settings_dict[setting_key] = cls._parseCompatibleValue(um_setting_entry.text) + + # Unknown settings + else: + Logger.log("w", "Unsupported material setting %s", setting_key) + + # Fetch settings in the "cura" namespace + cura_settings = node.iterfind("./cura:setting", cls.__namespaces) + for cura_setting_entry in cura_settings: + value = cura_setting_entry.text + if value.lower() == "yes": + value = True + elif value.lower() == "no": + value = False + key = cura_setting_entry.get("key") + + # Cura settings are all mapped + node_mapped_settings_dict[key] = value + + return node_mapped_settings_dict, node_unmapped_settings_dict + @classmethod def deserializeMetadata(cls, serialized: str, container_id: str) -> List[Dict[str, Any]]: result_metadata = [] #All the metadata that we found except the base (because the base is returned). @@ -985,6 +1050,36 @@ class XmlMaterialProfile(InstanceContainer): result_metadata.append(new_hotend_material_metadata) + # + # Buildplates in Hotends + # + buildplates = hotend.iterfind("./um:buildplate", cls.__namespaces) + for buildplate in buildplates: + # The "id" field for buildplate in material profiles is actually name + buildplate_name = buildplate.get("id") + if buildplate_name is None: + continue + + buildplate_mapped_settings, buildplate_unmapped_settings = cls._getSettingsDictForNode(buildplate) + buildplate_compatibility = buildplate_unmapped_settings.get("hardware compatible", + buildplate_map["buildplate_compatible"]) + buildplate_recommended = buildplate_unmapped_settings.get("hardware recommended", + buildplate_map["buildplate_recommended"]) + + # Generate container ID for the hotend-and-buildplate-specific material container + new_hotend_and_buildplate_specific_material_id = new_hotend_specific_material_id + "_" + buildplate_name.replace( + " ", "_") + + new_hotend_and_buildplate_material_metadata = {} + new_hotend_and_buildplate_material_metadata.update(new_hotend_material_metadata) + new_hotend_and_buildplate_material_metadata["id"] = new_hotend_and_buildplate_specific_material_id + new_hotend_and_buildplate_material_metadata["buildplate_name"] = buildplate_name + new_hotend_and_buildplate_material_metadata["compatible"] = buildplate_compatibility + new_hotend_and_buildplate_material_metadata["buildplate_compatible"] = buildplate_compatibility + new_hotend_and_buildplate_material_metadata["buildplate_recommended"] = buildplate_recommended + + result_metadata.append(new_hotend_and_buildplate_material_metadata) + # there is only one ID for a machine. Once we have reached here, it means we have already found # a workable ID for that machine, so there is no need to continue break diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index b767aac7b9..6134a43b8e 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2127,6 +2127,7 @@ "type": "float", "default_value": 60, "value": "default_material_bed_temperature", + "resolve": "max(extruderValues('material_bed_temperature'))", "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "130",