Merge pull request #6542 from Ultimaker/CURA-6793_performance

Performance improvements
This commit is contained in:
ninovanhooff 2019-10-23 16:50:04 +02:00 committed by GitHub
commit b9b086a8c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 145 additions and 163 deletions

View File

@ -126,10 +126,6 @@ class BuildVolume(SceneNode):
# Therefore this works. # Therefore this works.
self._machine_manager.activeQualityChanged.connect(self._onStackChanged) self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
# This should also ways work, and it is semantically more correct,
# but it does not update the disallowed areas after material change
self._machine_manager.activeStackChanged.connect(self._onStackChanged)
# Enable and disable extruder # Enable and disable extruder
self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck) self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)

View File

@ -1,21 +1,22 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job # For our background task of loading MachineNodes lazily.
from UM.JobQueue import JobQueue # For our background task of loading MachineNodes lazily.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added. from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added.
from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal from UM.Signal import Signal
import cura.CuraApplication # Imported like this to prevent circular dependencies. import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.MachineNode import MachineNode from cura.Machines.MachineNode import MachineNode
from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added. from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
from typing import Dict, List, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING
import time import time
import UM.FlameProfiler
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Machines.QualityGroup import QualityGroup from cura.Machines.QualityGroup import QualityGroup
from cura.Machines.QualityChangesGroup import QualityChangesGroup from cura.Machines.QualityChangesGroup import QualityChangesGroup
from UM.Settings.ContainerStack import ContainerStack
## This class contains a look-up tree for which containers are available at ## This class contains a look-up tree for which containers are available at
@ -38,12 +39,9 @@ class ContainerTree:
return cls.__instance return cls.__instance
def __init__(self) -> None: def __init__(self) -> None:
self.machines = {} # type: Dict[str, MachineNode] # Mapping from definition ID to machine nodes. self.machines = self._MachineNodeMap() # Mapping from definition ID to machine nodes with lazy loading.
self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed. self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed.
cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed.
container_registry = ContainerRegistry.getInstance()
container_registry.containerAdded.connect(self._machineAdded)
self._loadAll()
## Get the quality groups available for the currently activated printer. ## Get the quality groups available for the currently activated printer.
# #
@ -76,54 +74,85 @@ class ContainerTree:
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled) return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
# Add a machine node by the id of it's definition. ## Ran after completely starting up the application.
# This is automatically called by the _machineAdded function, but it's sometimes needed to add a machine node def _onStartupFinished(self):
# faster than would have been done when waiting on any signals (for instance; when creating an entirely new machine) currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
@UM.FlameProfiler.profile JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
def addMachineNodeByDefinitionId(self, definition_id: str) -> None:
if definition_id in self.machines:
return # Already have this definition ID.
start_time = time.time() ## Dictionary-like object that contains the machines.
self.machines[definition_id] = MachineNode(definition_id) #
self.machines[definition_id].materialsChanged.connect(self.materialsChanged) # This handles the lazy loading of MachineNodes.
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time)) class _MachineNodeMap:
def __init__(self) -> None:
self._machines = {} # type: Dict[str, MachineNode]
## Builds the initial container tree. ## Returns whether a printer with a certain definition ID exists. This
def _loadAll(self): # is regardless of whether or not the printer is loaded yet.
Logger.log("i", "Building container tree.") # \param definition_id The definition to look for.
start_time = time.time() # \return Whether or not a printer definition exists with that name.
all_stacks = ContainerRegistry.getInstance().findContainerStacks() def __contains__(self, definition_id: str) -> bool:
for stack in all_stacks: return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0
if not isinstance(stack, GlobalStack):
continue # Only want to load global stacks. We don't need to create a tree for extruder definitions.
definition_id = stack.definition.getId()
if definition_id not in self.machines:
definition_start_time = time.time()
self.machines[definition_id] = MachineNode(definition_id)
self.machines[definition_id].materialsChanged.connect(self.materialsChanged)
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - definition_start_time))
Logger.log("d", "Building the container tree took %s seconds", time.time() - start_time) ## Returns a machine node for the specified definition ID.
#
# If the machine node wasn't loaded yet, this will load it lazily.
# \param definition_id The definition to look for.
# \return A machine node for that definition.
def __getitem__(self, definition_id: str) -> MachineNode:
if definition_id not in self._machines:
start_time = time.time()
self._machines[definition_id] = MachineNode(definition_id)
self._machines[definition_id].materialsChanged.connect(ContainerTree.getInstance().materialsChanged)
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
return self._machines[definition_id]
## When a printer gets added, we need to build up the tree for that container. ## Gets a machine node for the specified definition ID, with default.
def _machineAdded(self, container_stack: ContainerInterface) -> None: #
if not isinstance(container_stack, GlobalStack): # The default is returned if there is no definition with the specified
return # Not our concern. # ID. If the machine node wasn't loaded yet, this will load it lazily.
self.addMachineNodeByDefinitionId(container_stack.definition.getId()) # \param definition_id The definition to look for.
# \param default The machine node to return if there is no machine
# with that definition (can be ``None`` optionally or if not
# provided).
# \return A machine node for that definition, or the default if there
# is no definition with the provided definition_id.
def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]:
if definition_id not in self:
return default
return self[definition_id]
## For debugging purposes, visualise the entire container tree as it stands ## Returns whether we've already cached this definition's node.
# now. # \param definition_id The definition that we may have cached.
def _visualise_tree(self) -> str: # \return ``True`` if it's cached.
lines = ["% CONTAINER TREE"] # Start with array and then combine into string, for performance. def is_loaded(self, definition_id: str) -> bool:
for machine in self.machines.values(): return definition_id in self._machines
lines.append(" # " + machine.container_id)
for variant in machine.variants.values(): ## Pre-loads all currently added printers as a background task so that
lines.append(" * " + variant.container_id) # switching printers in the interface is faster.
for material in variant.materials.values(): class _MachineNodeLoadJob(Job):
lines.append(" + " + material.container_id) ## Creates a new background task.
for quality in material.qualities.values(): # \param tree_root The container tree instance. This cannot be
lines.append(" - " + quality.container_id) # obtained through the singleton static function since the instance
for intent in quality.intents.values(): # may not yet be constructed completely.
lines.append(" . " + intent.container_id) # \param container_stacks All of the stacks to pre-load the container
return "\n".join(lines) # trees for. This needs to be provided from here because the stacks
# need to be constructed on the main thread because they are QObject.
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]):
self.tree_root = tree_root
self.container_stacks = container_stacks
super().__init__()
## Starts the background task.
#
# The ``JobQueue`` will schedule this on a different thread.
def run(self) -> None:
for stack in self.container_stacks: # Load all currently-added containers.
if not isinstance(stack, GlobalStack):
continue
# Allow a thread switch after every container.
# Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
# We're in no hurry though. Half a second is fine.
time.sleep(0.5)
definition_id = stack.definition.getId()
if not self.tree_root.machines.is_loaded(definition_id):
_ = self.tree_root.machines[definition_id]

View File

@ -177,5 +177,7 @@ class MachineNode(ContainerNode):
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer. global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
if len(global_qualities) == 0: # This printer doesn't override the global qualities. if len(global_qualities) == 0: # This printer doesn't override the global qualities.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities. global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
if len(global_qualities) == 0: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()]
for global_quality in global_qualities: for global_quality in global_qualities:
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self) self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)

View File

@ -53,7 +53,7 @@ class BaseMaterialsModel(ListModel):
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._updateExtruderStack() self._updateExtruderStack()
# Update this model when switching machines, when adding materials or changing their metadata. # Update this model when switching machines or tabs, when adding materials or changing their metadata.
self._machine_manager.activeStackChanged.connect(self._onChanged) self._machine_manager.activeStackChanged.connect(self._onChanged)
ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged) ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged)
self._application.getMaterialManagementModel().favoritesChanged.connect(self._onChanged) self._application.getMaterialManagementModel().favoritesChanged.connect(self._onChanged)
@ -140,8 +140,8 @@ class BaseMaterialsModel(ListModel):
if material_base_file in self._available_materials: if material_base_file in self._available_materials:
self._onChanged() self._onChanged()
## This is an abstract method that needs to be implemented by the specific ## This is an abstract method that needs to be implemented by the specific
# models themselves. # models themselves.
def _update(self): def _update(self):
self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";")) self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
@ -155,7 +155,7 @@ class BaseMaterialsModel(ListModel):
nozzle_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
materials = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[nozzle_name].materials materials = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[nozzle_name].materials
approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter() approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter()
self._available_materials = {key: material for key, material in materials.items() if float(material.container.getMetaDataEntry("approximate_diameter")) == approximate_material_diameter} self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter}
## This method is used by all material models in the beginning of the ## This method is used by all material models in the beginning of the
# _update() method in order to prevent errors. It's the same in all models # _update() method in order to prevent errors. It's the same in all models

View File

@ -27,7 +27,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(container_node.container.getMetaDataEntry("removed", False)): if bool(container_node.getMetaDataEntry("removed", False)):
continue continue
# Only add results for favorite materials # Only add results for favorite materials

View File

@ -18,11 +18,11 @@ class GenericMaterialsModel(BaseMaterialsModel):
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(container_node.container.getMetaDataEntry("removed", False)): if bool(container_node.getMetaDataEntry("removed", False)):
continue continue
# Only add results for generic materials # Only add results for generic materials
if container_node.container.getMetaDataEntry("brand", "unknown").lower() != "generic": if container_node.getMetaDataEntry("brand", "unknown").lower() != "generic":
continue continue
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)

View File

@ -61,11 +61,9 @@ class IntentCategoryModel(ListModel):
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange) ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange)
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange) ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange)
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager = application.getMachineManager() machine_manager.activeMaterialChanged.connect(self.update)
machine_manager.globalContainerChanged.connect(self.update) machine_manager.activeVariantChanged.connect(self.update)
machine_manager.activeQualityGroupChanged.connect(self.update)
machine_manager.activeStackChanged.connect(self.update)
machine_manager.extruderChanged.connect(self.update) machine_manager.extruderChanged.connect(self.update)
extruder_manager = application.getExtruderManager() extruder_manager = application.getExtruderManager()

View File

@ -37,18 +37,18 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Part 1: Generate the entire tree of brands -> material types -> spcific materials # Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(container_node.container.getMetaDataEntry("removed", False)): if bool(container_node.getMetaDataEntry("removed", False)):
continue continue
# Add brands we haven't seen yet to the dict, skipping generics # Add brands we haven't seen yet to the dict, skipping generics
brand = container_node.container.getMetaDataEntry("brand", "") brand = container_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
continue continue
if brand not in brand_group_dict: if brand not in brand_group_dict:
brand_group_dict[brand] = {} brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict # Add material types we haven't seen yet to the dict
material_type = container_node.container.getMetaDataEntry("material", "") material_type = container_node.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]: if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = [] brand_group_dict[brand][material_type] = []

View File

@ -3,9 +3,9 @@
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
@ -21,16 +21,13 @@ class NozzleModel(ListModel):
self.addRoleName(self.HotendNameRole, "hotend_name") self.addRoleName(self.HotendNameRole, "hotend_name")
self.addRoleName(self.ContainerNodeRole, "container_node") self.addRoleName(self.ContainerNodeRole, "container_node")
self._application = Application.getInstance() cura.CuraApplication.CuraApplication.getInstance().getMachineManager().globalContainerChanged.connect(self._update)
self._machine_manager = self._application.getMachineManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._update() self._update()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
self.setItems([]) self.setItems([])
return return

View File

@ -40,7 +40,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
application.globalContainerStackChanged.connect(self._onChange) application.globalContainerStackChanged.connect(self._onChange)
machine_manager.activeQualityGroupChanged.connect(self._onChange) machine_manager.activeQualityGroupChanged.connect(self._onChange)
machine_manager.activeStackChanged.connect(self._onChange) machine_manager.activeMaterialChanged.connect(self._onChange)
machine_manager.activeVariantChanged.connect(self._onChange)
machine_manager.extruderChanged.connect(self._onChange) machine_manager.extruderChanged.connect(self._onChange)
extruder_manager = application.getExtruderManager() extruder_manager = application.getExtruderManager()

View File

@ -68,7 +68,7 @@ class QualityGroup:
if not node.container: if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id)) Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False)) is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental self.is_experimental |= is_experimental
def setExtruderNode(self, position: int, node: "ContainerNode") -> None: def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
@ -78,5 +78,5 @@ class QualityGroup:
if not node.container: if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id)) Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False)) is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental self.is_experimental |= is_experimental

View File

@ -85,7 +85,7 @@ class ContainerManager(QObject):
if container_node.container is None: if container_node.container is None:
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id)) Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
return False return False
root_material_id = container_node.container.getMetaDataEntry("base_file", "") root_material_id = container_node.getMetaDataEntry("base_file", "")
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
if container_registry.isReadOnly(root_material_id): if container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id) Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
@ -350,8 +350,7 @@ class ContainerManager(QObject):
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def unlinkMaterial(self, material_node: "MaterialNode") -> None: def unlinkMaterial(self, material_node: "MaterialNode") -> None:
# Get the material group # Get the material group
if material_node.container is None: if material_node.container is None: # Failed to lazy-load this container.
Logger.log("w", "Material node {0} doesn't have a container.".format(material_node.container_id))
return return
root_material_query = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findInstanceContainers(id = material_node.getMetaDataEntry("base_file", "")) root_material_query = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findInstanceContainers(id = material_node.getMetaDataEntry("base_file", ""))
if not root_material_query: if not root_material_query:

View File

@ -37,10 +37,6 @@ class CuraStackBuilder:
return None return None
machine_definition = definitions[0] machine_definition = definitions[0]
# The container tree listens to the containerAdded signal to add the definition and build the tree,
# but that signal is emitted with a delay which might not have passed yet.
# Therefore we must make sure that it's manually added here.
container_tree.addMachineNodeByDefinitionId(machine_definition.getId())
machine_node = container_tree.machines[machine_definition.getId()] machine_node = container_tree.machines[machine_definition.getId()]
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName()) generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -42,8 +42,6 @@ class ExtruderManager(QObject):
# TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point. # TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point.
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes. ## Signal to notify other components when the list of extruders for a machine definition changes.
@ -311,38 +309,33 @@ class ExtruderManager(QObject):
self.resetSelectedObjectExtruders() self.resetSelectedObjectExtruders()
## Adds the extruders of the currently active machine. ## Adds the extruders to the selected machine.
def _addCurrentMachineExtruders(self) -> None: def addMachineExtruders(self, global_stack: GlobalStack) -> None:
global_stack = self._application.getGlobalContainerStack()
extruders_changed = False extruders_changed = False
container_registry = ContainerRegistry.getInstance()
global_stack_id = global_stack.getId()
if global_stack: # Gets the extruder trains that we just created as well as any that still existed.
container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
global_stack_id = global_stack.getId()
# Gets the extruder trains that we just created as well as any that still existed. # Make sure the extruder trains for the new machine can be placed in the set of sets
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id) if global_stack_id not in self._extruder_trains:
self._extruder_trains[global_stack_id] = {}
extruders_changed = True
# Make sure the extruder trains for the new machine can be placed in the set of sets # Register the extruder trains by position
if global_stack_id not in self._extruder_trains: for extruder_train in extruder_trains:
self._extruder_trains[global_stack_id] = {} extruder_position = extruder_train.getMetaDataEntry("position")
extruders_changed = True self._extruder_trains[global_stack_id][extruder_position] = extruder_train
# Register the extruder trains by position # regardless of what the next stack is, we have to set it again, because of signal routing. ???
for extruder_train in extruder_trains: extruder_train.setParent(global_stack)
extruder_position = extruder_train.getMetaDataEntry("position") extruder_train.setNextStack(global_stack)
self._extruder_trains[global_stack_id][extruder_position] = extruder_train extruders_changed = True
# regardless of what the next stack is, we have to set it again, because of signal routing. ??? self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
extruder_train.setParent(global_stack) if extruders_changed:
extruder_train.setNextStack(global_stack) self.extrudersChanged.emit(global_stack_id)
extruders_changed = True
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if extruders_changed:
self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0)
self.activeExtruderChanged.emit()
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.

View File

@ -139,8 +139,8 @@ class MachineManager(QObject):
activeVariantChanged = pyqtSignal() activeVariantChanged = pyqtSignal()
activeQualityChanged = pyqtSignal() activeQualityChanged = pyqtSignal()
activeIntentChanged = pyqtSignal() activeIntentChanged = pyqtSignal()
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value) activeStackChanged = pyqtSignal() # Emitted whenever the active extruder stack is changed (ie: when switching the active extruder tab or changing between printers)
extruderChanged = pyqtSignal() extruderChanged = pyqtSignal() # Emitted whenever an extruder is activated or deactivated or the default extruder changes.
activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed. activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed.
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
@ -217,6 +217,7 @@ class MachineManager(QObject):
return 0 return 0
return len(general_definition_containers[0].getAllKeys()) return len(general_definition_containers[0].getAllKeys())
## Triggered when the global container stack is changed in CuraApplication.
def _onGlobalContainerChanged(self) -> None: def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack: if self._global_container_stack:
try: try:
@ -268,11 +269,7 @@ class MachineManager(QObject):
def _onActiveExtruderStackChanged(self) -> None: def _onActiveExtruderStackChanged(self) -> None:
self.blurSettings.emit() # Ensure no-one has focus. self.blurSettings.emit() # Ensure no-one has focus.
if self._active_container_stack is not None:
self._active_container_stack.pyqtContainersChanged.disconnect(self.activeStackChanged) # Unplug from the old one.
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack() self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if self._active_container_stack is not None:
self._active_container_stack.pyqtContainersChanged.connect(self.activeStackChanged) # Plug into the new one.
def __emitChangedSignals(self) -> None: def __emitChangedSignals(self) -> None:
self.activeQualityChanged.emit() self.activeQualityChanged.emit()
@ -296,7 +293,6 @@ class MachineManager(QObject):
self.blurSettings.emit() # Ensure no-one has focus. self.blurSettings.emit() # Ensure no-one has focus.
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
containers = container_registry.findContainerStacks(id = stack_id) containers = container_registry.findContainerStacks(id = stack_id)
if not containers: if not containers:
return return
@ -306,21 +302,25 @@ class MachineManager(QObject):
# Make sure that the default machine actions for this machine have been added # Make sure that the default machine actions for this machine have been added
self._application.getMachineActionManager().addDefaultMachineActions(global_stack) self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack) extruder_manager = ExtruderManager.getInstance()
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid(): if not global_stack.isValid():
# Mark global stack as invalid # Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here return # We're done here
self._global_container_stack = global_stack self._global_container_stack = global_stack
extruder_manager.addMachineExtruders(global_stack)
self._application.setGlobalContainerStack(global_stack) self._application.setGlobalContainerStack(global_stack)
ExtruderManager.getInstance()._globalContainerStackChanged()
self._onGlobalContainerChanged()
# Switch to the first enabled extruder # Switch to the first enabled extruder
self.updateDefaultExtruder() self.updateDefaultExtruder()
default_extruder_position = int(self.defaultExtruderPosition) default_extruder_position = int(self.defaultExtruderPosition)
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position) old_active_extruder_index = extruder_manager.activeExtruderIndex
extruder_manager.setActiveExtruderIndex(default_extruder_position)
if old_active_extruder_index == default_extruder_position:
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
extruder_manager.activeExtruderChanged.emit()
self.__emitChangedSignals() self.__emitChangedSignals()

View File

@ -29,6 +29,7 @@ def createMockedStack(definition_id: str):
def container_registry(): def container_registry():
result = MagicMock() result = MagicMock()
result.findContainerStacks = MagicMock(return_value = [createMockedStack("machine_1"), createMockedStack("machine_2")]) result.findContainerStacks = MagicMock(return_value = [createMockedStack("machine_1"), createMockedStack("machine_2")])
result.findContainersMetadata = lambda id: [{"id": id}] if id in {"machine_1", "machine_2"} else []
return result return result
@pytest.fixture @pytest.fixture
@ -37,41 +38,11 @@ def application():
def test_containerTreeInit(container_registry): def test_containerTreeInit(container_registry):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
container_tree = ContainerTree()
assert "machine_1" in container_tree.machines
assert "machine_2" in container_tree.machines
def test_newMachineAdded(container_registry):
mocked_definition_container = MagicMock(spec=DefinitionContainer)
mocked_definition_container.getId = MagicMock(return_value = "machine_3")
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)): with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
container_tree = ContainerTree() container_tree = ContainerTree()
# machine_3 shouldn't be there, as on init it wasn't in the registry
assert "machine_3" not in container_tree.machines
# It should only react when a globalStack is added. assert "machine_1" in container_tree.machines
container_tree._machineAdded(mocked_definition_container) assert "machine_2" in container_tree.machines
assert "machine_3" not in container_tree.machines
container_tree._machineAdded(createMockedStack("machine_3"))
assert "machine_3" in container_tree.machines
def test_alreadyKnownMachineAdded(container_registry):
mocked_definition_container = MagicMock(spec = DefinitionContainer)
mocked_definition_container.getId = MagicMock(return_value = "machine_2")
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
container_tree = ContainerTree()
assert len(container_tree.machines) == 2
# The ID is already there, so no machine should be added.
container_tree._machineAdded(mocked_definition_container)
assert len(container_tree.machines) == 2
def test_getCurrentQualityGroupsNoGlobalStack(container_registry): def test_getCurrentQualityGroupsNoGlobalStack(container_registry):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)): with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
@ -84,7 +55,7 @@ def test_getCurrentQualityGroupsNoGlobalStack(container_registry):
def test_getCurrentQualityGroups(container_registry, application): def test_getCurrentQualityGroups(container_registry, application):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)): with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
container_tree = ContainerTree() container_tree = ContainerTree()
container_tree.machines["current_global_stack"] = MagicMock() # Mock so that we can track whether the getQualityGroups function gets called with correct parameters. container_tree.machines._machines["current_global_stack"] = MagicMock() # Mock so that we can track whether the getQualityGroups function gets called with correct parameters.
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)): with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)):
result = container_tree.getCurrentQualityGroups() result = container_tree.getCurrentQualityGroups()
@ -108,7 +79,7 @@ def test_getCurrentQualityChangesGroupsNoGlobalStack(container_registry):
def test_getCurrentQualityChangesGroups(container_registry, application): def test_getCurrentQualityChangesGroups(container_registry, application):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)): with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
container_tree = ContainerTree() container_tree = ContainerTree()
container_tree.machines["current_global_stack"] = MagicMock() # Mock so that we can track whether the getQualityGroups function gets called with correct parameters. container_tree.machines._machines["current_global_stack"] = MagicMock() # Mock so that we can track whether the getQualityGroups function gets called with correct parameters.
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)): with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)):
result = container_tree.getCurrentQualityChangesGroups() result = container_tree.getCurrentQualityChangesGroups()