diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index f5a8995af1..25130fe255 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -1,4 +1,4 @@ -from collections import defaultdict +from collections import defaultdict, OrderedDict from typing import Optional from PyQt5.Qt import QTimer, QObject, pyqtSignal @@ -17,6 +17,9 @@ class MaterialGroup: self.root_material_node = None self.derived_material_node_list = [] + def __str__(self) -> str: + return "%s[%s]" % (self.__class__.__name__, self.name) + class MaterialNode(ContainerNode): __slots__ = ("material_map", "children_map") @@ -45,6 +48,9 @@ class MaterialManager(QObject): self._material_diameter_map = defaultdict() # root_material_id -> diameter -> root_material_id for that diameter self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) + # This is used in Legacy UM3 send material function and the material management page. + self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups + # 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" @@ -63,7 +69,7 @@ class MaterialManager(QObject): # Find all materials and put them in a matrix for quick search. material_metadata_list = self._container_registry.findContainersMetadata(type = "material") - self._material_group_map = {} + self._material_group_map = OrderedDict() self._diameter_machine_variant_material_map = {} # Map #1 @@ -85,6 +91,14 @@ class MaterialManager(QObject): else: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) + self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) + + # Map #1.5 + # GUID -> material group list + self._guid_material_groups_map = defaultdict(list) + for root_material_id, material_group in self._material_group_map.items(): + guid = material_group.root_material_node.metadata["GUID"] + self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata @@ -198,6 +212,9 @@ class MaterialManager(QObject): def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id) + def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: + return self._guid_material_groups_map.get(guid) + # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # @@ -206,29 +223,27 @@ class MaterialManager(QObject): rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_variant_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) - return {} + return dict() # 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 machine_node is None: - machine_node = machine_variant_material_map.get(self._default_machine_definition_id) if variant_name is not None and machine_node is not None: variant_node = machine_node.getChildNode(variant_name) + nodes_to_check = [variant_node, machine_node, default_machine_node] + # Fallback mechanism of finding materials: # 1. variant-specific material # 2. machine-specific material # 3. generic material (for fdmprinter) - material_id_metadata_dict = {} - if variant_node is not None: - material_id_metadata_dict = {mid: node for mid, node in variant_node.material_map.items()} - - # Fallback: machine-specific materials, including "fdmprinter" - if not material_id_metadata_dict: - if machine_node is not None: - material_id_metadata_dict = {mid: node for mid, node in machine_node.material_map.items()} + material_id_metadata_dict = dict() + for node in nodes_to_check: + if node is not None: + material_id_metadata_dict = {mid: node for mid, node in variant_node.material_map.items()} + break return material_id_metadata_dict diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 4c92eed845..7d4c702cf4 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -764,16 +764,16 @@ class ContainerManager(QObject): ## Create a duplicate of a material, which has the same GUID and base_file metadata # # \return \type{str} the id of the newly created container. - @pyqtSlot(str, result = str) - def duplicateMaterial(self, material_id: str) -> str: - assert material_id + @pyqtSlot("QVariant") + def duplicateMaterial(self, material_node): + root_material_id = material_node.metadata["base_file"] from cura.CuraApplication import CuraApplication material_manager = CuraApplication.getInstance()._material_manager - material_group = material_manager.getMaterialGroup(material_id) + material_group = material_manager.getMaterialGroup(root_material_id) if not material_group: - Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id) + Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return "" base_container = material_group.root_material_node.getContainer() @@ -803,7 +803,7 @@ class ContainerManager(QObject): if container_to_copy.getMetaDataEntry("variant_name"): variant_name = container_to_copy.getMetaDataEntry("variant_name") new_id += "_" + variant_name.replace(" ", "_") - if current_id == material_id: + if current_id == root_material_id: clone_of_original = new_id new_container = copy.deepcopy(container_to_copy) @@ -814,17 +814,6 @@ class ContainerManager(QObject): for container_to_add in new_containers: container_to_add.setDirty(True) ContainerRegistry.getInstance().addContainer(container_to_add) - return self._getMaterialContainerIdForActiveMachine(clone_of_original) - - ## Create a duplicate of a material or it's original entry - # - # \return \type{str} the id of the newly created container. - @pyqtSlot(str, result = str) - def duplicateOriginalMaterial(self, material_id): - - # check if the given material has a base file (i.e. was shipped by default) - base_file = self.getContainerMetaDataEntry(material_id, "base_file") - return self.duplicateMaterial(base_file) ## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue # @@ -869,72 +858,43 @@ class ContainerManager(QObject): duplicated_container.setName(catalog.i18nc("@label", "Custom Material")) self._container_registry.addContainer(duplicated_container) - return self._getMaterialContainerIdForActiveMachine(new_id) - - ## Find the id of a material container based on the new material - # Utilty function that is shared between duplicateMaterial and createMaterial - # - # \param base_file \type{str} the id of the created container. - def _getMaterialContainerIdForActiveMachine(self, base_file): - global_stack = Application.getInstance().getGlobalContainerStack() - if not global_stack: - return base_file - - has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False)) - has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False)) - has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False)) - if has_machine_materials or has_variant_materials: - if has_variants: - materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId) - else: - materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId()) - - if materials: - return materials[0]["id"] - - Logger.log("w", "Unable to find a suitable container based on %s for the current machine.", base_file) - return "" # do not activate a new material if a container can not be found - - return base_file ## 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. # \return \type{list} a list of names of materials with the same GUID - @pyqtSlot(str, result = "QStringList") - def getLinkedMaterials(self, material_id: str): - containers = self._container_registry.findInstanceContainersMetadata(id = material_id) - if not containers: - Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id) - return [] + @pyqtSlot("QVariant", result = "QStringList") + def getLinkedMaterials(self, material_node): + guid = material_node.metadata["GUID"] - material_container = containers[0] - material_base_file = material_container.get("base_file", "") - material_guid = material_container.get("GUID", "") - if not material_guid: - Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id) - return [] + from cura.CuraApplication import CuraApplication + material_manager = CuraApplication.getInstance()._material_manager + + material_group_list = material_manager.getMaterialGroupListByGUID(guid) - containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_guid) linked_material_names = [] - for container in containers: - if container["id"] in [material_id, material_base_file] or container.get("base_file") != container["id"]: - continue - - linked_material_names.append(container["name"]) + if material_group_list: + for material_group in material_group_list: + linked_material_names.append(material_group.root_material_node.metadata["name"]) return linked_material_names ## 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. - @pyqtSlot(str) - def unlinkMaterial(self, material_id: str): - containers = self._container_registry.findInstanceContainers(id=material_id) - if not containers: - Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id) - return "" + @pyqtSlot("QVariant") + def unlinkMaterial(self, material_node): + # Get the material group + from cura.CuraApplication import CuraApplication + material_manager = CuraApplication.getInstance()._material_manager + material_group = material_manager.getMaterialGroup(material_node.metadata["base_file"]) - containers[0].setMetaDataEntry("GUID", str(uuid.uuid4())) + # Generate a new GUID + new_guid = str(uuid.uuid4()) + # Update the GUID + # NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will + # take care of the derived containers too + container = material_group.root_material_node.getContainer() + container.setMetaDataEntry("GUID", new_guid) ## Get the singleton instance for this class. @classmethod diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index f35f15b737..338a72434f 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -48,18 +48,28 @@ class XmlMaterialProfile(InstanceContainer): ## Overridden from InstanceContainer # set the meta data for all machine / variant combinations - def setMetaDataEntry(self, key, value): + def setMetaDataEntry(self, key, value, is_first_call = True): registry = ContainerRegistry.getInstance() if registry.isReadOnly(self.getId()): return - super().setMetaDataEntry(key, value) + # Prevent recursion + if is_first_call: + super().setMetaDataEntry(key, value) - basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile. - # Update all containers that share basefile - for container in registry.findInstanceContainers(base_file = basefile): - if container.getMetaDataEntry(key, None) != value: # Prevent recursion - container.setMetaDataEntry(key, value) + # Get the MaterialGroup + material_manager = CuraApplication.getInstance()._material_manager + root_material_id = self.getMetaDataEntry("base_file") #if basefile is self.getId, this is a basefile. + material_group = material_manager.getMaterialGroup(root_material_id) + + # Update the root material container + root_material_container = material_group.root_material_node.getContainer() + root_material_container.setMetaDataEntry(key, value, is_first_call = False) + + # Update all containers derived from it + for node in material_group.derived_material_node_list: + container = node.getContainer() + container.setMetaDataEntry(key, value, is_first_call = False) ## Overridden from InstanceContainer, similar to setMetaDataEntry. # without this function the setName would only set the name of the specific nozzle / material / machine combination container diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 50eb3d3e99..f07564f7a5 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -12,7 +12,8 @@ TabView { id: base - property QtObject properties; + property QtObject properties + property var currentMaterialNode: null property bool editingEnabled: false; property string currency: UM.Preferences.getValue("cura/currency") ? UM.Preferences.getValue("cura/currency") : "€" @@ -27,15 +28,16 @@ TabView property bool reevaluateLinkedMaterials: false property string linkedMaterialNames: { - if (reevaluateLinkedMaterials) - { + if (reevaluateLinkedMaterials) { reevaluateLinkedMaterials = false; } - if(!base.containerId || !base.editingEnabled) - { + if (!base.containerId || !base.editingEnabled) { + return "" + } + var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode); + if (linkedMaterials.length <= 1) { return "" } - var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.containerId); return linkedMaterials.join(", "); } @@ -80,18 +82,18 @@ TabView { id: textField; width: scrollView.columnWidth; - text: properties.supplier; + text: properties.brand; readOnly: !base.editingEnabled; - onEditingFinished: base.updateMaterialSupplier(properties.supplier, text) + onEditingFinished: base.updateMaterialBrand(properties.brand, text) } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") } ReadOnlyTextField { width: scrollView.columnWidth; - text: properties.material_type; + text: properties.material; readOnly: !base.editingEnabled; - onEditingFinished: base.updateMaterialType(properties.material_type, text) + onEditingFinished: base.updateMaterialType(properties.material, text) } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } @@ -251,7 +253,7 @@ TabView visible: base.linkedMaterialNames != "" onClicked: { - Cura.ContainerManager.unlinkMaterial(base.containerId) + Cura.ContainerManager.unlinkMaterial(base.currentMaterialNode) base.reevaluateLinkedMaterials = true } } @@ -466,12 +468,12 @@ TabView // update the type of the material function updateMaterialType (old_type, new_type) { base.setMetaDataEntry("material", old_type, new_type) - materialProperties.material_type = new_type + materialProperties.material= new_type } - // update the supplier of the material - function updateMaterialSupplier (old_supplier, new_supplier) { - base.setMetaDataEntry("brand", old_supplier, new_supplier) - materialProperties.supplier = new_supplier + // update the brand of the material + function updateMaterialBrand (old_brand, new_brand) { + base.setMetaDataEntry("brand", old_brand, new_brand) + materialProperties.brand = new_brand } } diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index ae90c13e8c..9fbd8a5042 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -34,6 +34,8 @@ Item text: catalog.i18nc("@title:tab", "Materials") } + property var hasCurrentItem: materialListView.currentItem != null; + property var currentItem: { var current_index = materialListView.currentIndex; @@ -65,9 +67,6 @@ Item onClicked: { forceActiveFocus() - var current_index = materialListView.currentIndex; - var item = materialsModel.getItem(current_index); - const extruder_position = Cura.ExtruderManager.activeExtruderIndex; Cura.MachineManager.setMaterial(extruder_position, base.currentItem.container_node); } @@ -87,10 +86,11 @@ Item Button { text: catalog.i18nc("@action:button", "Duplicate"); iconName: "list-add" - enabled: true //TODO + enabled: base.hasCurrentItem onClicked: { forceActiveFocus() - // TODO + + Cura.ContainerManager.duplicateMaterial(base.currentItem.container_node); } } @@ -277,6 +277,7 @@ Item { var model = materialsModel.getItem(currentIndex); materialDetailsView.containerId = model.container_id; + materialDetailsView.currentMaterialNode = model.container_node; detailsPanel.updateMaterialPropertiesObject(); } @@ -303,8 +304,8 @@ Item materialProperties.name = currentItem.name; materialProperties.guid = currentItem.guid; - materialProperties.supplier = currentItem.brand ? currentItem.brand : "Unknown"; - materialProperties.material_type = currentItem.material ? currentItem.material : "Unknown"; + materialProperties.brand = currentItem.brand ? currentItem.brand : "Unknown"; + materialProperties.material = currentItem.material ? currentItem.material : "Unknown"; materialProperties.color_name = currentItem.color_name ? currentItem.color_name : "Yellow"; materialProperties.color_code = currentItem.color_code ? currentItem.color_code : "yellow"; @@ -358,6 +359,7 @@ Item properties: materialProperties containerId: base.currentItem != null ? base.currentItem.id : "" + currentMaterialNode: base.currentItem property alias pane: base } @@ -369,8 +371,9 @@ Item property string guid: "00000000-0000-0000-0000-000000000000" property string name: "Unknown"; property string profile_type: "Unknown"; - property string supplier: "Unknown"; - property string material_type: "Unknown"; + property string brand: "Unknown"; + property string material: "Unknown"; // This needs to be named as "material" to be consistent with + // the material container's metadata entry property string color_name: "Yellow"; property color color_code: "yellow";