diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7812024422..82a112f56f 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -52,7 +52,8 @@ from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyT from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.SettingFunction import SettingFunction from cura.Settings.MachineNameValidator import MachineNameValidator -from cura.Settings.ProfilesModel import ProfilesModel +from cura.Settings.NozzleModel import NozzleModel +from cura.Settings.ProfilesModel import ProfilesModel, NewQualityProfilesModel from cura.Settings.MaterialsModel import MaterialsModel, BrandMaterialsModel, GenericMaterialsModel from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel from cura.Settings.SettingInheritanceManager import SettingInheritanceManager @@ -60,6 +61,7 @@ from cura.Settings.UserProfilesModel import UserProfilesModel from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager +from cura.Machines.QualityManager import QualityGroup from . import PlatformPhysics @@ -738,6 +740,10 @@ class CuraApplication(QtApplication): self._material_manager = MaterialManager(container_registry) self._material_manager.initialize() + from cura.Machines.QualityManager import QualityManager + self._quality_manager = QualityManager(container_registry) + self._quality_manager.initialize() + # Check if we should run as single instance or not self._setUpSingleInstanceServer() @@ -922,6 +928,9 @@ class CuraApplication(QtApplication): qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel") + qmlRegisterType(NewQualityProfilesModel, "Cura", 1, 0, "NewQualityProfilesModel") + qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel") + qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel") qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel") qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel") diff --git a/cura/Machines/ContainerGroup.py b/cura/Machines/ContainerGroup.py new file mode 100644 index 0000000000..5c34f0f123 --- /dev/null +++ b/cura/Machines/ContainerGroup.py @@ -0,0 +1,10 @@ +from PyQt5.Qt import QObject + +from cura.Machines.ContainerNode import ContainerNode + + +class ContainerGroup(QObject): + def __init__(self, parent = None): + super().__init__(parent) + self.node_for_global = None # type: ContainerNode + self.nodes_for_extruders = dict() diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py new file mode 100644 index 0000000000..43556dbb4a --- /dev/null +++ b/cura/Machines/QualityManager.py @@ -0,0 +1,358 @@ +from typing import Optional + +from PyQt5.Qt import pyqtSignal, QObject + +from UM.Application import Application +from UM.Logger import Logger +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Util import parseBool + +from cura.Machines.ContainerGroup import ContainerGroup +from cura.Machines.ContainerNode import ContainerNode + + +# +# Quality lookup tree structure: +# +# ------| +# | | +# +# | +# +# | +# +# | +# +# + +# + + +class QualityChangesGroup: + __slots__ = ("name", "qc_for_global", "qc_for_extruders_dict") + + def __init__(self, name: str): + self.name = name # type: str + self.qc_for_global = None # type: Optional["QualityNode"] + self.qc_for_extruders_dict = dict() # -> QualityNode + + def addMetadata(self, metadata: dict): + extruder_id = metadata.get("extruder") + if extruder_id is not None: + self.qc_for_extruders_dict[extruder_id] = QualityNode(metadata) + else: + self.qc_for_global = QualityNode(metadata) + + def getContainerForGlobalStack(self) -> "InstanceContainer": + return self.qc_for_global.getContainer() + + def getContainerForExtruderStack(self, extruder_definition_id: str) -> Optional["InstanceContainer"]: + qc_node = self.qc_for_extruders_dict.get(extruder_definition_id) + container = None + if qc_node is not None: + container = qc_node.getContainer() + return container + + +class QualityGroup(ContainerGroup): + + def __init__(self, name, quality_type, parent = None): + super().__init__(parent) + self.name = name + self.quality_type = quality_type + self.is_available = False + + +class QualityNode(ContainerNode): + __slots__ = ("metadata", "container", "quality_type_map", "children_map") + + def __init__(self, metadata = None): + super().__init__(metadata = metadata) + self.quality_type_map = {} + + + def addQualityMetadata(self, quality_type: str, metadata: dict): + if quality_type not in self.quality_type_map: + self.quality_type_map[quality_type] = QualityNode(metadata) + + def getQualityNode(self, quality_type: str): + return self.quality_type_map.get(quality_type) + + def addQualityChangesMetadata(self, quality_type: str, metadata: dict): + if quality_type not in self.quality_type_map: + self.quality_type_map[quality_type] = QualityNode() + quality_type_node = self.quality_type_map[quality_type] + + name = metadata["name"] + if name not in quality_type_node.children_map: + quality_type_node.children_map[name] = QualityChangesGroup(name) + qc_group = quality_type_node.children_map[name] + qc_group.addMetadata(metadata) + + +class QualityManager(QObject): + + def __init__(self, container_registry, parent = None): + super().__init__(parent) + + self._material_manager = Application.getInstance()._material_manager + + self._container_registry = container_registry + self._empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0] + #self._empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] + + self._machine_variant_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" + + def initialize(self): + # Initialize the lookup tree for quality profiles with following structure: + # -> -> + # -> + quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality") + for metadata in quality_metadata_list: + if metadata["id"] == "empty_quality": + continue + + definition_id = metadata["definition"] + quality_type = metadata["quality_type"] + + root_material_id = metadata.get("material") + variant_name = metadata.get("variant") + 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) + + # 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): + raise RuntimeError("Quality profile [%s] contains invalid data: it is a global quality but contains 'material' and 'nozzle' info." % metadata["id"]) + + 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 = self._machine_variant_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 = machine_node.children_map[variant_name] + + 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 = variant_node.children_map[root_material_id] + + material_node.addQualityMetadata(quality_type, metadata) + + else: + # If variant_name 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() + material_node = machine_node.children_map[root_material_id] + + material_node.addQualityMetadata(quality_type, metadata) + + # Initialize the lookup tree for quality_changes profiles with following structure: + # -> -> + quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes") + for metadata in quality_changes_metadata_list: + if metadata["id"] == "empty_quality_changes": + continue + + machine_definition_id = metadata["definition"] + quality_type = metadata["quality_type"] + + if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict: + self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode() + machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id] + + machine_node.addQualityChangesMetadata(quality_type, metadata) + + def getQualityGroups(self, machine: "GlobalStack") -> dict: + # TODO: How to make this simpler, including the fallbacks. + # Get machine definition ID + machine_definition_id = self._default_machine_definition_id + if parseBool(machine.getMetaDataEntry("has_machine_quality", False)): + machine_definition_id = machine.getMetaDataEntry("quality_definition") + if machine_definition_id is None: + machine_definition_id = machine.definition.getId() + + machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) + if not machine_node: + Logger.log("e", "Cannot find node for machine def [%s] in quality lookup table", machine_definition_id) + + # iterate over all quality_types in the machine node + quality_group_dict = {} + for quality_type, quality_node in machine_node.quality_type_map.items(): + quality_group = QualityGroup(quality_node.metadata["name"], quality_type) + quality_group.node_for_global = quality_node + + quality_group_dict[quality_type] = quality_group + + # Iterate over all extruders + for position, extruder in machine.extruders.items(): + variant_name = None + if extruder.variant.getId() != "empty_variant": + variant_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. + root_material_id_list = [] + has_material = False # flag indicating whether this extruder has a material assigned + if extruder.material.getId() != "empty_material": + has_material = True + root_material_id = extruder.material.getMetaDataEntry("base_file") + root_material_id_list.append(root_material_id) + + # Also try to get the fallback material + material_type = extruder.material.getMetaDataEntry("material") + fallback_root_material_metadata = self._material_manager.getFallbackMaterialForType(material_type) + if fallback_root_material_metadata: + root_material_id_list.append(fallback_root_material_metadata["id"]) + + variant_node = None + material_node = None + + if variant_name: + # In this case, we have both a specific variant and a specific material + variant_node = machine_node.getChildNode(variant_name) + if not variant_node: + continue + if has_material: + for root_material_id in root_material_id_list: + material_node = variant_node.getChildNode(root_material_id) + if material_node: + break + if not material_node: + # No suitable quality found: not supported + Logger.log("d", "Cannot find quality with machine [%s], variant name [%s], and materials [%s].", + machine_definition_id, variant_name, ", ".join(root_material_id_list)) + continue + else: + # In this case, we only have a specific material but NOT a variant + if has_material: + for root_material_id in root_material_id_list: + material_node = machine_node.getChildNode(root_material_id) + if material_node: + break + if not material_node: + # No suitable quality found: not supported + Logger.log("d", "Cannot find quality with machine [%s], variant name [%s], and materials [%s].", + machine_definition_id, variant_name, ", ".join(root_material_id_list)) + continue + + node_to_check = material_node + if not node_to_check: + node_to_check = variant_node + if not node_to_check: + node_to_check = machine_node + + for quality_type, quality_node in node_to_check.quality_type_map.items(): + if quality_type not in quality_group_dict: + quality_group = QualityGroup(quality_node.metadata["name"], quality_type) + quality_group_dict[quality_type] = quality_group + + quality_group = quality_group_dict[quality_type] + + quality_group.nodes_for_extruders[position] = quality_node + + used_extruders = set() + # TODO: This will change after the Machine refactoring + for i in range(machine.getProperty("machine_extruder_count", "value")): + used_extruders.add(str(i)) + + # Update the "is_available" flag for each quality group. + for quality_group in quality_group_dict.values(): + is_available = True + if quality_group.node_for_global is None: + is_available = False + if is_available: + for position in used_extruders: + if position not in quality_group.nodes_for_extruders: + is_available = False + break + + quality_group.is_available = is_available + + return quality_group_dict + + + def getQuality(self, quality_type: str, machine: "GlobalStack"): + # Get machine definition ID + machine_definition_id = self._default_machine_definition_id + if parseBool(machine.getMetaDataEntry("has_machine_quality", False)): + machine_definition_id = machine.getMetaDataEntry("quality_definition") + if machine_definition_id is None: + machine_definition_id = machine.definition.getId() + + machine_quality = self.getQualityContainer(quality_type, machine_definition_id) + extruder_quality_dict = {} + for position, extruder in machine.extruders.items(): + variant = extruder.variant + material = extruder.material + + variant_name = variant.getName() + if variant.getId() == "empty_variant": + variant_name = None + root_material_id = material.getMetaDataEntry("base_file") + if material.getId() == "empty_material": + root_material_id = None + + extruder_quality = self.getQualityContainer(quality_type, machine_definition_id, + variant_name, root_material_id) + extruder_quality_dict[position] = extruder_quality + + # TODO: return as a group + return machine_quality, extruder_quality_dict + + def getQualityContainer(self, quality_type: str, machine_definition_id: dict, + variant_name: Optional[str] = None, + root_material_id: Optional[str] = None) -> "InstanceContainer": + assert machine_definition_id is not None + assert quality_type is not None + + # If the specified quality cannot be found, empty_quality will be returned. + container = self._empty_quality_container + + machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) + variant_node = None + material_node = None + if machine_node is not None: + if variant_name is not None: + variant_node = machine_node.getChildNode(variant_name) + if variant_node is not None: + if root_material_id is not None: + material_node = variant_node.getChildNode(root_material_id) + elif root_material_id is not None: + material_node = machine_node.getChildNode(root_material_id) + + nodes_to_try = [material_node, variant_node, machine_node] + if machine_definition_id != self._default_machine_definition_id: + default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) + nodes_to_try.append(default_machine_node) + + for node in nodes_to_try: + if node is None: + continue + quality_node = node.getQualityNode(quality_type) + if quality_node is None: + continue + + container = quality_node.getContainer() + break + + return container + diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1fa4a602bc..f0305751d4 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1509,12 +1509,12 @@ class MachineManager(QObject): self._global_container_stack.extruders[position].variant = container_node.getContainer() @pyqtSlot("QVariant") - def handleQualityGroup(self, quality_group): - Logger.log("d", "---------------- qg = [%s]", quality_group.name) + def handleQualityGroup(self, qg): + Logger.log("d", "---------------- qg = [%s]", qg.name) self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): - self._global_container_stack.quality = quality_group.node_for_global.getContainer() + self._global_container_stack.quality = qg.node_for_global.getContainer() self._global_container_stack.qualityChanges = self._empty_quality_changes_container - for position, node in quality_group.nodes_for_extruders.items(): + for position, node in qg.nodes_for_extruders.items(): self._global_container_stack.extruders[position].quality = node.getContainer() self._global_container_stack.extruders[position].qualityChanges = self._empty_quality_changes_container diff --git a/cura/Settings/ProfilesModel.py b/cura/Settings/ProfilesModel.py index 77cd407457..719a406406 100644 --- a/cura/Settings/ProfilesModel.py +++ b/cura/Settings/ProfilesModel.py @@ -18,6 +18,62 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack +from UM.Qt.ListModel import ListModel + + +class NewQualityProfilesModel(ListModel): + IdRole = Qt.UserRole + 1 + NameRole = Qt.UserRole + 2 + QualityTypeRole = Qt.UserRole + 3 + LayerHeightRole = Qt.UserRole + 4 + AvailableRole = Qt.UserRole + 5 + QualityGroupRole = Qt.UserRole + 6 + + def __init__(self, parent = None): + super().__init__(parent) + + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.QualityTypeRole, "quality_type") + self.addRoleName(self.LayerHeightRole, "layer_height") + self.addRoleName(self.AvailableRole, "available") + self.addRoleName(self.QualityGroupRole, "quality_group") + + # TODO: connect signals + Application.getInstance().globalContainerStackChanged.connect(self._update) + Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update) + Application.getInstance().getMachineManager().activeStackChanged.connect(self._update) + Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update) + + def _update(self): + # TODO: get all available qualities + self.items.clear() + + quality_manager = Application.getInstance()._quality_manager + active_global_stack = Application.getInstance().getMachineManager()._global_container_stack + if active_global_stack is None: + self.setItems([]) + return + + quality_group_dict = quality_manager.getQualityGroups(active_global_stack) + + item_list = [] + + for key in sorted(quality_group_dict): + quality_group = quality_group_dict[key] + + item = {"id": "TODO", + "name": quality_group.name, + "layer_height": "TODO", + "available": quality_group.is_available, + "quality_group": quality_group} + + item_list.append(item) + + self.setItems(item_list) + + + ## QML Model for listing the current list of valid quality profiles. # class ProfilesModel(InstanceContainersModel): diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 826960a621..74e4a9551e 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -17,6 +17,7 @@ "has_variants": true, "preferred_variant": "*aa04*", "preferred_quality": "*Normal*", + "preferred_quality_type": "fine", "variants_name": "Print core", "machine_extruder_trains": { diff --git a/resources/qml/Menus/ProfileMenu.qml b/resources/qml/Menus/ProfileMenu.qml index f543bb36eb..bde60a3dde 100644 --- a/resources/qml/Menus/ProfileMenu.qml +++ b/resources/qml/Menus/ProfileMenu.qml @@ -11,9 +11,15 @@ Menu { id: menu + // TODO: single instance + Cura.NewQualityProfilesModel + { + id: qualityProfilesModel + } + Instantiator { - model: Cura.ProfilesModel + model: qualityProfilesModel MenuItem { @@ -21,7 +27,10 @@ Menu checkable: true checked: Cura.MachineManager.activeQualityId == model.id exclusiveGroup: group - onTriggered: Cura.MachineManager.setActiveQuality(model.id) + onTriggered: { + Cura.MachineManager.handleQualityGroup(model.quality_group) + //Cura.MachineManager.setActiveQuality(model.id) + } visible: model.available }