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

This commit is contained in:
Jaime van Kessel 2019-08-29 16:28:26 +02:00
commit b00b8c8c58
27 changed files with 603 additions and 239 deletions

View file

@ -4,6 +4,7 @@
from typing import TYPE_CHECKING
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Machines.ContainerNode import ContainerNode
if TYPE_CHECKING:
@ -16,5 +17,4 @@ class IntentNode(ContainerNode):
def __init__(self, container_id: str, quality: "QualityNode") -> None:
super().__init__(container_id)
self.quality = quality
my_metadata = ContainerRegistry.getInstance().findContainersMetadata(id=container_id)[0]
self.intent_category = my_metadata.get("intent_category", "default")
self.intent_category = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0].get("intent_category", "default")

View file

@ -36,7 +36,7 @@ class MachineNode(ContainerNode):
self.has_variants = parseBool(my_metadata.get("has_variants", "false"))
self.has_machine_materials = parseBool(my_metadata.get("has_machine_materials", "false"))
self.has_machine_quality = parseBool(my_metadata.get("has_machine_quality", "false"))
self.quality_definition = my_metadata.get("quality_definition", container_id)
self.quality_definition = my_metadata.get("quality_definition", container_id) if self.has_machine_quality else "fdmprinter"
self.exclude_materials = my_metadata.get("exclude_materials", [])
self.preferred_variant_name = my_metadata.get("preferred_variant_name", "")
self.preferred_material = my_metadata.get("preferred_material", "")
@ -62,7 +62,7 @@ class MachineNode(ContainerNode):
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]]
qualities_per_type_per_extruder = [{}] * 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.
@ -77,6 +77,9 @@ class MachineNode(ContainerNode):
# Create the quality group for each available type.
quality_groups = {}
for quality_type, global_quality_node in self.global_qualities.items():
if not global_quality_node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(global_quality_node.container_id))
continue
quality_groups[quality_type] = QualityGroup(name = global_quality_node.container.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
quality_groups[quality_type].node_for_global = global_quality_node
for extruder, qualities_per_type in enumerate(qualities_per_type_per_extruder):
@ -116,7 +119,7 @@ class MachineNode(ContainerNode):
def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]:
machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder.
groups_by_name = {} # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
for quality_changes in machine_quality_changes:
name = quality_changes["name"]
if name not in groups_by_name:
@ -143,7 +146,7 @@ class MachineNode(ContainerNode):
# quality is taken.
# If there are no global qualities, an empty quality is returned.
def preferredGlobalQuality(self) -> QualityNode:
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities)))
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
## (Re)loads all variants under this printer.
def _loadAll(self):

View file

@ -1,5 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
import copy
import uuid
@ -7,19 +8,16 @@ from typing import Dict, Optional, TYPE_CHECKING, Any, List, cast
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction
from UM.Util import parseBool
import cura.CuraApplication #Imported like this to prevent circular imports.
import cura.CuraApplication # Imported like this to prevent circular imports.
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from .MaterialNode import MaterialNode
from .MaterialGroup import MaterialGroup
from .VariantType import VariantType
if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer
@ -134,7 +132,7 @@ class MaterialManager(QObject):
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
materials = self.getAvailableMaterials(machine.definition.getId(), nozzle_name)
compatible_material_diameter = str(round(extruder_stack.getCompatibleMaterialDiameter()))
result = {key: material for key, material in materials.items() if material.container.getMetaDataEntry("approximate_diameter") == compatible_material_diameter}
result = {key: material for key, material in materials.items() if material.container and material.container.getMetaDataEntry("approximate_diameter") == compatible_material_diameter}
return result
#
@ -248,7 +246,7 @@ class MaterialManager(QObject):
def removeMaterialByRootId(self, root_material_id: str):
container_registry = CuraContainerRegistry.getInstance()
results = container_registry.findContainers(id=root_material_id)
results = container_registry.findContainers(id = root_material_id)
if not results:
container_registry.addWrongContainerId(root_material_id)
@ -260,8 +258,8 @@ class MaterialManager(QObject):
# 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
# corrupts the configuration)
root_material_id = material_node.container.getMetaDataEntry("base_file")
ids_to_remove = [metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file=root_material_id)]
root_material_id = material_node.base_file
ids_to_remove = {metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file = root_material_id)}
for extruder_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
@ -270,6 +268,8 @@ class MaterialManager(QObject):
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
if material_node.container is None:
return
root_material_id = material_node.container.getMetaDataEntry("base_file")
if root_material_id is None:
return
@ -281,6 +281,8 @@ class MaterialManager(QObject):
@pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode") -> None:
if material_node.container is None:
return
root_material_id = material_node.container.getMetaDataEntry("base_file")
if root_material_id is not None:
self.removeMaterialByRootId(root_material_id)
@ -300,7 +302,6 @@ class MaterialManager(QObject):
# Create a new ID & container to hold the data.
new_containers = []
container_registry = CuraContainerRegistry.getInstance()
if new_base_id is None:
new_base_id = container_registry.uniqueName(base_container.getId())
new_base_container = copy.deepcopy(base_container)
@ -336,15 +337,19 @@ class MaterialManager(QObject):
# if the duplicated material was favorite then the new material should also be added to favorite.
if root_material_id in self.getFavorites():
self.addFavorite(new_base_id)
cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().addFavorite(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]:
if material_node.container is None:
Logger.log("e", "Material node {0} doesn't have container.".format(material_node.container_id))
return "ERROR"
root_material_id = cast(str, material_node.container.getMetaDataEntry("base_file", ""))
return self.duplicateMaterialByRootId(root_material_id, new_base_id, new_metadata)
@ -361,7 +366,11 @@ class MaterialManager(QObject):
machine_manager = application.getMachineManager()
extruder_stack = machine_manager.activeStack
machine_definition = application.getGlobalContainerStack().definition
global_stack = application.getGlobalContainerStack()
if global_stack is None:
Logger.log("e", "Global stack not present!")
return "ERROR"
machine_definition = global_stack.definition
root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)

View file

@ -62,15 +62,16 @@ class MaterialNode(ContainerNode):
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter")
else:
# Need to find the qualities that specify a material profile with the same material type.
my_material_type = self.material_type
qualities = []
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):
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.
my_guid = self.guid
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name, material = self.container_id) # First try by exact material ID.
if not qualities:
my_material_type = self.material_type
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):
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.
my_guid = self.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"]))
for quality in qualities:
quality_id = quality["id"]

View file

@ -31,8 +31,13 @@ class BaseMaterialsModel(ListModel):
self._container_registry = self._application.getInstance().getContainerRegistry()
self._machine_manager = self._application.getMachineManager()
self._extruder_position = 0
self._extruder_stack = None
self._enabled = True
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._updateExtruderStack()
# Update this model when switching machines, when adding materials or changing their metadata.
self._machine_manager.activeStackChanged.connect(self._update)
@ -55,12 +60,8 @@ class BaseMaterialsModel(ListModel):
self.addRoleName(Qt.UserRole + 15, "container_node")
self.addRoleName(Qt.UserRole + 16, "is_favorite")
self._extruder_position = 0
self._extruder_stack = None
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
self._favorite_ids = set() # type: Set[str]
self._enabled = True
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
@ -95,7 +96,7 @@ class BaseMaterialsModel(ListModel):
self._update()
self.enabledChanged.emit()
@pyqtProperty(bool, fset=setEnabled, notify=enabledChanged)
@pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
def enabled(self):
return self._enabled
@ -107,7 +108,10 @@ class BaseMaterialsModel(ListModel):
return
if material.variant.container_id != self._extruder_stack.variant.getId():
return
if material.variant.machine.container_id != cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().definition.getId():
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return
if material.variant.machine.container_id != global_stack.definition.getId():
return
self._update()
@ -167,4 +171,3 @@ class BaseMaterialsModel(ListModel):
"is_favorite": root_material_id in self._favorite_ids
}
return item

View file

@ -19,11 +19,7 @@ class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
return
variant_names = [extruder.variant.getName() for extruder in active_global_stack.extruders.values()]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in active_global_stack.extruders.values()]
extruder_enabled = [extruder.isEnabled for extruder in active_global_stack.extruders.values()]
machine_node = ContainerTree.getInstance().machines[active_global_stack.definition.getId()]
quality_changes_list = machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
item_list = []
for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):

View file

@ -33,6 +33,8 @@ class IntentModel(ListModel):
self._intent_category = "engineering"
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.globalContainerChanged.connect(self._update)
ContainerRegistry.getInstance().containerAdded.connect(self._onChanged)
ContainerRegistry.getInstance().containerRemoved.connect(self._onChanged)
self._layer_height_unit = "" # This is cached

View file

@ -0,0 +1,169 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials.
from PyQt5.QtCore import QObject, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials.
from UM.i18n import i18nCatalog
from UM.Logger import Logger
import cura.CuraApplication # Imported like this to prevent circular imports.
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
if TYPE_CHECKING:
from cura.Machines.MaterialNode import MaterialNode
catalog = i18nCatalog("cura")
## Proxy class to the materials page in the preferences.
#
# This class handles the actions in that page, such as creating new materials,
# renaming them, etc.
class MaterialManagementModel(QObject):
## Can a certain material be deleted, or is it still in use in one of the
# container stacks anywhere?
#
# We forbid the user from deleting a material if it's in use in any stack.
# Deleting it while it's in use can lead to corrupted stacks. In the
# future we might enable this functionality again (deleting the material
# from those stacks) but for now it is easier to prevent the user from
# doing this.
# \param material_node The ContainerTree node of the material to check.
# \return Whether or not the material can be removed.
@pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
container_registry = CuraContainerRegistry.getInstance()
ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
## Change the user-visible name of a material.
# \param material_node The ContainerTree node of the material to rename.
# \param name The new name for the material.
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
container_registry = CuraContainerRegistry.getInstance()
root_material_id = material_node.base_file
if container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
return
return container_registry.findContainers(id = root_material_id)[0].setName(name)
## Deletes a material from Cura.
#
# This function does not do any safety checking any more. Please call this
# function only if:
# - The material is not read-only.
# - The material is not used in any stacks.
# If the material was not lazy-loaded yet, this will fully load the
# container. When removing this material node, all other materials with
# the same base fill will also be removed.
# \param material_node The material to remove.
@pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode") -> None:
container_registry = CuraContainerRegistry.getInstance()
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
for material_metadata in materials_this_base_file:
container_registry.removeContainer(material_metadata["id"])
## Creates a duplicate of a material with the same GUID and base_file
# metadata.
# \param material_node The node representing the material to duplicate.
# \param new_base_id A new material ID for the base material. The IDs of
# the submaterials will be based off this one. If not provided, a material
# ID will be generated automatically.
# \param new_metadata Metadata for the new material. If not provided, this
# will be duplicated from the original material.
# \return The root material ID of the duplicate material.
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
container_registry = CuraContainerRegistry.getInstance()
root_materials = container_registry.findContainers(id = material_node.base_file)
if not root_materials:
Logger.log("i", "Unable to duplicate the root material with ID {root_id}, because it doesn't exist.".format(root_id = material_node.base_file))
return None
root_material = root_materials[0]
# Ensure that all settings are saved.
application = cura.CuraApplication.CuraApplication.getInstance()
application.saveSettings()
# Create a new ID and container to hold the data.
if new_base_id is None:
new_base_id = container_registry.uniqueName(root_material.getId())
new_root_material = copy.deepcopy(root_material)
new_root_material.getMetaData()["id"] = new_base_id
new_root_material.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
new_root_material.getMetaData().update(new_metadata)
new_containers = [new_root_material]
# Clone all submaterials.
for container_to_copy in container_registry.findInstanceContainers(base_file = material_node.base_file):
if container_to_copy.getId() == material_node.base_file:
continue # We already have that one. Skip it.
new_id = new_base_id
definition = container_to_copy.getMetaDataEntry("definition")
if definition != "fdmprinter":
new_id += "_" + definition
variant_name = container_to_copy.getMetaDataEntry("variant_name")
if variant_name:
new_id += "_" + variant_name.replace(" ", "_")
new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id
new_container.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
new_container.getMetaData().update(new_metadata)
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
container_registry.addContainer(container_to_add)
# If the duplicated material was favorite then the new material should also be added to the favorites.
favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
if material_node.base_file in favorites_set:
favorites_set.add(new_base_id)
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
return new_base_id
## Create a new material by cloning the preferred material for the current
# material diameter and generate a new GUID.
#
# The material type is explicitly left to be the one from the preferred
# material, since this allows the user to still have SOME profiles to work
# with.
# \return The ID of the newly created material.
@pyqtSlot(result = str)
def createMaterial(self) -> str:
# Ensure all settings are saved.
application = cura.CuraApplication.CuraApplication.getInstance()
application.saveSettings()
# Find the preferred material.
extruder_stack = application.getMachineManager().activeStack
active_variant_name = extruder_stack.variant.getName()
approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
global_container_stack = application.getGlobalContainerStack()
if not global_container_stack:
return ""
machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
# Create a new ID & new metadata for the new material.
new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
"brand": catalog.i18nc("@label", "Custom"),
"GUID": str(uuid.uuid4()),
}
self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
return new_id

View file

@ -1,11 +1,22 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSlot
from typing import Any, Dict, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, Qt
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.InstanceContainer import InstanceContainer # To create new profiles.
import cura.CuraApplication # Imported this way to prevent circular imports.
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
if TYPE_CHECKING:
from UM.Settings.Interfaces import ContainerInterface
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
#
# This the QML model for the quality management page.
@ -24,17 +35,136 @@ class QualityManagementModel(ListModel):
self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._quality_manager = CuraApplication.getInstance().getQualityManager()
application = cura.CuraApplication.CuraApplication.getInstance()
container_registry = application.getContainerRegistry()
self._machine_manager = application.getMachineManager()
self._extruder_manager = application.getExtruderManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._quality_manager.qualitiesUpdated.connect(self._update)
container_registry.containerAdded.connect(self._qualityChangesListChanged)
container_registry.containerRemoved.connect(self._qualityChangesListChanged)
container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
self._update()
## Deletes a custom profile. It will be gone forever.
# \param quality_changes_group The quality changes group representing the
# profile to delete.
@pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
removed_quality_changes_ids = set()
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
container_id = metadata["id"]
container_registry.removeContainer(container_id)
removed_quality_changes_ids.add(container_id)
# Reset all machines that have activated this custom profile.
for global_stack in container_registry.findContainerStacks(type = "machine"):
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
global_stack.qualityChanges = empty_quality_changes_container
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = empty_quality_changes_container
## Rename a custom profile.
#
# Because the names must be unique, the new name may not actually become
# the name that was given. The actual name is returned by this function.
# \param quality_changes_group The custom profile that must be renamed.
# \param new_name The desired name for the profile.
# \return The actual new name of the profile, after making the name
# unique.
@pyqtSlot(QObject, str, result = str)
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
if new_name == quality_changes_group.name:
Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
return new_name
application = cura.CuraApplication.CuraApplication.getInstance()
new_name = application.getContainerRegistry().uniqueName(new_name)
for node in quality_changes_group.getAllNodes():
container = node.container
if container:
container.setName(new_name)
quality_changes_group.name = new_name
application.getMachineManager().activeQualityChanged.emit()
application.getMachineManager().activeQualityGroupChanged.emit()
return new_name
## Duplicates a given quality profile OR quality changes profile.
# \param new_name The desired name of the new profile. This will be made
# unique, so it might end up with a different name.
# \param quality_model_item The item of this model to duplicate, as
# dictionary. See the descriptions of the roles of this list model.
@pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
return
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
new_name = container_registry.uniqueName(new_name)
quality_group = quality_model_item["quality_group"]
quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None:
# Create global quality changes only.
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name, global_stack, extruder_stack = None)
container_registry.addContainer(new_quality_changes)
else:
for metadata in [quality_changes_group.metadata_for_global] + quality_changes_group.metadata_per_extruder.values():
containers = container_registry.findContainers(id = metadata["id"])
if not containers:
continue
container = containers[0]
new_id = container_registry.uniqueName(container.getId())
container_registry.addContainer(container.duplicate(new_id, new_name))
## Create a quality changes container with the given set-up.
# \param quality_type The quality type of the new container.
# \param new_name The name of the container. This name must be unique.
# \param machine The global stack to create the profile for.
# \param extruder_stack The extruder stack to create the profile for. If
# not provided, only a global container will be created.
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
new_id = base_id + "_" + new_name
new_id = new_id.lower().replace(" ", "_")
new_id = container_registry.uniqueName(new_id)
# Create a new quality_changes container for the quality.
quality_changes = InstanceContainer(new_id)
quality_changes.setName(new_name)
quality_changes.setMetaDataEntry("type", "quality_changes")
quality_changes.setMetaDataEntry("quality_type", quality_type)
# If we are creating a container for an extruder, ensure we add that to the container.
if extruder_stack is not None:
quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
machine_definition_id = ContainerTree.getInstance().machines[machine.definition.getId()].quality_definition
quality_changes.setDefinition(machine_definition_id)
quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
return quality_changes
## Triggered when any container changed.
#
# This filters the updates to the container manager: When it applies to
# the list of quality changes, we need to update our list.
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
if container.getMetaDataEntry("type") == "quality_changes":
self._update()
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
@ -43,12 +173,13 @@ class QualityManagementModel(ListModel):
self.setItems([])
return
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
container_tree = ContainerTree.getInstance()
quality_group_dict = container_tree.getCurrentQualityGroups()
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
if quality_group.is_available)
if not available_quality_types and not quality_changes_group_dict:
if not available_quality_types and not quality_changes_group_list:
# Nothing to show
self.setItems([])
return
@ -69,7 +200,7 @@ class QualityManagementModel(ListModel):
# Create quality_changes group items
quality_changes_item_list = []
for quality_changes_group in quality_changes_group_dict.values():
for quality_changes_group in quality_changes_group_list:
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
item = {"name": quality_changes_group.name,
"is_read_only": False,

View file

@ -1,9 +1,9 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Application import Application
import cura.CuraApplication
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -35,15 +35,13 @@ class QualitySettingsModel(ListModel):
self.addRoleName(self.CategoryRole, "category")
self._container_registry = ContainerRegistry.getInstance()
self._application = Application.getInstance()
self._quality_manager = self._application.getQualityManager()
self._application = cura.CuraApplication.CuraApplication.getInstance()
self._application.getMachineManager().activeStackChanged.connect(self._update)
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
self._selected_quality_item = None # The selected quality in the quality management page
self._i18n_catalog = None
self._quality_manager.qualitiesUpdated.connect(self._update)
self._update()
selectedPositionChanged = pyqtSignal()
@ -93,21 +91,33 @@ class QualitySettingsModel(ListModel):
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
settings_keys = quality_group.getAllKeys()
quality_containers = []
if quality_node is not None and quality_node.getContainer() is not None:
quality_containers.append(quality_node.getContainer())
if quality_node is not None and quality_node.container is not None:
quality_containers.append(quality_node.container)
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
# the settings in that quality_changes_group.
if quality_changes_group is not None:
if self._selected_position == self.GLOBAL_STACK_POSITION:
quality_changes_node = quality_changes_group.node_for_global
container_registry = ContainerRegistry.getInstance()
global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])
global_container = None if len(global_containers) == 0 else global_containers[0]
extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder}
extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()}
if self._selected_position == self.GLOBAL_STACK_POSITION and global_container:
quality_changes_metadata = global_container.getMetaData()
else:
quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position))
if quality_changes_node is not None and quality_changes_node.container is not None: # it can be None if number of extruders are changed during runtime
quality_containers.insert(0, quality_changes_node.container)
settings_keys.update(quality_changes_group.getAllKeys())
quality_changes_metadata = extruders_container.get(str(self._selected_position))
if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
container = container_registry.findContainers(id = quality_changes_metadata["id"])
if container:
quality_containers.insert(0, container[0])
# We iterate over all definitions instead of settings in a quality/qualtiy_changes group is because in the GUI,
if global_container:
settings_keys.update(global_container.getAllKeys())
for container in extruders_container.values():
if container:
settings_keys.update(container.getAllKeys())
# We iterate over all definitions instead of settings in a quality/quality_changes group is because in the GUI,
# the settings are grouped together by categories, and we had to go over all the definitions to figure out
# which setting belongs in which category.
current_category = ""

View file

@ -16,7 +16,7 @@ class QualityChangesGroup(QObject):
self.quality_type = quality_type
self.intent_category = intent_category
self.is_available = False
self.metadata_for_global = None # type: Optional[str]
self.metadata_for_global = {} # type: Dict[str, Any]
self.metadata_per_extruder = {} # type: Dict[int, Dict[str, Any]]
def __str__(self) -> str:

View file

@ -5,6 +5,7 @@ from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode
@ -60,6 +61,9 @@ class QualityGroup(QObject):
self.node_for_global = node
# Update is_experimental flag
if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental
@ -67,5 +71,8 @@ class QualityGroup(QObject):
self.nodes_for_extruders[position] = node
# Update is_experimental flag
if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental

View file

@ -1,7 +1,7 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, Dict
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
@ -14,6 +14,7 @@ import cura.CuraApplication
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Machines.ContainerTree import ContainerTree # The implementation that replaces this manager, to keep the deprecated interface working.
from .QualityChangesGroup import QualityChangesGroup
from .QualityGroup import QualityGroup
from .QualityNode import QualityNode
@ -75,7 +76,7 @@ class QualityManager(QObject):
return
# Returns a dict of "custom profile name" -> QualityChangesGroup
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
def getQualityChangesGroups(self, machine: "GlobalStack") -> List[QualityChangesGroup]:
variant_names = [extruder.variant.getName() for extruder in machine.extruders.values()]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in machine.extruders.values()]
extruder_enabled = [extruder.isEnabled for extruder in machine.extruders.values()]
@ -111,12 +112,10 @@ class QualityManager(QObject):
# Iterate over all quality_types in the machine node
quality_group_dict = dict()
for node in nodes_to_check:
if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.setGlobalNode(quality_node)
quality_group_dict[quality_type] = quality_group
break
if node and node.quality_type:
quality_group = QualityGroup(node.getMetaDataEntry("name", ""), node.quality_type)
quality_group.setGlobalNode(node)
quality_group_dict[node.quality_type] = quality_group
return quality_group_dict
@ -142,76 +141,33 @@ class QualityManager(QObject):
# Methods for GUI
#
#
# Remove the given quality changes group.
#
## Deletes a custom profile. It will be gone forever.
# \param quality_changes_group The quality changes group representing the
# profile to delete.
@pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
removed_quality_changes_ids = set()
for node in quality_changes_group.getAllNodes():
container_id = node.container_id
self._container_registry.removeContainer(container_id)
removed_quality_changes_ids.add(container_id)
# Reset all machines that have activated this quality changes to empty.
for global_stack in self._container_registry.findContainerStacks(type = "machine"):
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
global_stack.qualityChanges = self._empty_quality_changes_container
for extruder_stack in self._container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = self._empty_quality_changes_container
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().removeQualityChangesGroup(quality_changes_group)
## Rename a custom profile.
#
# Rename a set of quality changes containers. Returns the new name.
#
# Because the names must be unique, the new name may not actually become
# the name that was given. The actual name is returned by this function.
# \param quality_changes_group The custom profile that must be renamed.
# \param new_name The desired name for the profile.
# \return The actual new name of the profile, after making the name
# unique.
@pyqtSlot(QObject, str, result = str)
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
Logger.log("i", "Renaming QualityChangesGroup[%s] to [%s]", quality_changes_group.name, new_name)
if new_name == quality_changes_group.name:
Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
return new_name
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().removeQualityChangesGroup(quality_changes_group, new_name)
new_name = self._container_registry.uniqueName(new_name)
for node in quality_changes_group.getAllNodes():
container = node.container
if container:
container.setName(new_name)
quality_changes_group.name = new_name
application = cura.CuraApplication.CuraApplication.getInstance()
application.getMachineManager().activeQualityChanged.emit()
application.getMachineManager().activeQualityGroupChanged.emit()
return new_name
#
# Duplicates the given quality.
#
## Duplicates a given quality profile OR quality changes profile.
# \param new_name The desired name of the new profile. This will be made
# unique, so it might end up with a different name.
# \param quality_model_item The item of this model to duplicate, as
# dictionary. See the descriptions of the roles of this list model.
@pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.")
return
quality_group = quality_model_item["quality_group"]
quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None:
# create global quality changes only
new_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
global_stack, None)
self._container_registry.addContainer(new_quality_changes)
else:
new_name = self._container_registry.uniqueName(quality_changes_name)
for node in quality_changes_group.getAllNodes():
container = node.container
if not container:
continue
new_id = self._container_registry.uniqueName(container.getId())
self._container_registry.addContainer(container.duplicate(new_id, new_name))
def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item: Dict[str, Any]) -> None:
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().duplicateQualityChanges(quality_changes_name, quality_model_item)
## Create quality changes containers from the user containers in the active stacks.
#

View file

@ -14,6 +14,7 @@ if TYPE_CHECKING:
from typing import Dict
from cura.Machines.MachineNode import MachineNode
## This class represents an extruder variant in the container tree.
#
# The subnodes of these nodes are materials.
@ -33,10 +34,11 @@ class VariantNode(ContainerNode):
container_registry = ContainerRegistry.getInstance()
self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"] # Store our own name so that we can filter more easily.
container_registry.containerAdded.connect(self._materialAdded)
container_registry.containerRemoved.connect(self._materialRemoved)
self._loadAll()
## (Re)loads all materials under this variant.
def _loadAll(self):
def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
if not self.machine.has_materials:
@ -53,12 +55,10 @@ class VariantNode(ContainerNode):
materials_per_base_file = {material["base_file"]: material for material in all_materials}
materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials}) # Variant-specific profiles override all of those.
materials = materials_per_base_file.values()
materials = list(materials_per_base_file.values())
filtered_materials = []
for material in materials:
if material["id"] not in self.machine.exclude_materials:
filtered_materials.append(material)
# Filter materials based on the exclude_materials property.
filtered_materials = [material for material in materials if material["id"] not in self.machine.exclude_materials]
for material in filtered_materials:
base_file = material["base_file"]
@ -79,7 +79,7 @@ class VariantNode(ContainerNode):
# material.
# \return The node for the preferred material, or any arbitrary material
# if there is no match.
def preferredMaterial(self, approximate_diameter) -> MaterialNode:
def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
for base_material, material_node in self.materials.items():
if self.machine.preferred_material in base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
return material_node
@ -94,7 +94,7 @@ class VariantNode(ContainerNode):
## When a material gets added to the set of profiles, we need to update our
# tree here.
def _materialAdded(self, container: ContainerInterface):
def _materialAdded(self, container: ContainerInterface) -> None:
if container.getMetaDataEntry("type") != "material":
return # Not interested.
if not self.machine.has_materials:
@ -127,4 +127,34 @@ class VariantNode(ContainerNode):
if "empty_material" in self.materials:
del self.materials["empty_material"]
self.materials[base_file] = MaterialNode(container.getId(), variant = self)
self.materials[base_file].materialChanged.connect(self.materialsChanged)
self.materials[base_file].materialChanged.connect(self.materialsChanged)
self.materialsChanged.emit(self.materials[base_file])
def _materialRemoved(self, container: ContainerInterface) -> None:
if container.getMetaDataEntry("type") != "material":
return # Only interested in materials.
base_file = container.getMetaDataEntry("base_file")
if base_file not in self.materials:
return # We don't track this material anyway. No need to remove it.
original_node = self.materials[base_file]
del self.materials[base_file]
self.materialsChanged.emit(original_node)
# Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
# Search for any submaterials from that base file that are still left.
materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
if materials_same_base_file:
most_specific_submaterial = materials_same_base_file[0]
for submaterial in materials_same_base_file:
if submaterial["definition"] == self.machine.container_id:
if most_specific_submaterial["definition"] == "fdmprinter":
most_specific_submaterial = submaterial
if most_specific_submaterial.get("variant", "empty") == "empty" and submaterial.get("variant", "empty") == self.variant_name:
most_specific_submaterial = submaterial
self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
self.materialsChanged.emit(self.materials[base_file])
if not self.materials: # The last available material just got deleted and there is nothing with the same base file to replace it.
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
self.materialsChanged.emit(self.materials["empty_material"])