diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f39c2ba554..e8d38d3942 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -67,6 +67,8 @@ from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel +from cura.Machines.MachineErrorChecker import MachineErrorChecker + from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager @@ -142,12 +144,6 @@ class CuraApplication(QtApplication): Q_ENUMS(ResourceTypes) - # FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it. - # Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager - # will make it initialized before ContainerRegistry does, and it won't find the active machine, thus - # Cura will always show the Add Machine Dialog upon start. - stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished - def __init__(self, **kwargs): # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]: @@ -224,12 +220,14 @@ class CuraApplication(QtApplication): self._machine_manager = None # This is initialized on demand. self._extruder_manager = None self._material_manager = None + self._quality_manager = None self._object_manager = None self._build_plate_model = None self._multi_build_plate_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None self._cura_scene_controller = None + self._machine_error_checker = None self._additional_components = {} # Components to add to certain areas in the interface @@ -743,19 +741,28 @@ class CuraApplication(QtApplication): self.preRun() container_registry = ContainerRegistry.getInstance() + + Logger.log("i", "Initializing variant manager") self._variant_manager = VariantManager(container_registry) self._variant_manager.initialize() + Logger.log("i", "Initializing material manager") from cura.Machines.MaterialManager import MaterialManager self._material_manager = MaterialManager(container_registry, parent = self) self._material_manager.initialize() + Logger.log("i", "Initializing quality manager") from cura.Machines.QualityManager import QualityManager self._quality_manager = QualityManager(container_registry, parent = self) self._quality_manager.initialize() + Logger.log("i", "Initializing machine manager") self._machine_manager = MachineManager(self) + Logger.log("i", "Initializing machine error checker") + self._machine_error_checker = MachineErrorChecker(self) + self._machine_error_checker.initialize() + # Check if we should run as single instance or not self._setUpSingleInstanceServer() @@ -781,8 +788,11 @@ class CuraApplication(QtApplication): self._openFile(file_name) self.started = True + self.initializationFinished.emit() self.exec_() + initializationFinished = pyqtSignal() + ## Run Cura without GUI elements and interaction (server mode). def runWithoutGUI(self): self._use_gui = False @@ -847,6 +857,9 @@ class CuraApplication(QtApplication): def hasGui(self): return self._use_gui + def getMachineErrorChecker(self, *args) -> MachineErrorChecker: + return self._machine_error_checker + def getMachineManager(self, *args) -> MachineManager: if self._machine_manager is None: self._machine_manager = MachineManager(self) diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py new file mode 100644 index 0000000000..871e70310b --- /dev/null +++ b/cura/Machines/MachineErrorChecker.py @@ -0,0 +1,184 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from collections import deque + +from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty + +from UM.Application import Application +from UM.Logger import Logger + + +# +# This class performs setting error checks for the currently active machine. +# +# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. +# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key +# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should +# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait +# for it to finish the complete work. +# +class MachineErrorChecker(QObject): + + def __init__(self, parent = None): + super().__init__(parent) + + self._global_stack = None + + self._has_errors = True # Result of the error check, indicating whether there are errors in the stack + self._error_keys = set() # A set of settings keys that have errors + self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check + + self._stacks_to_check = None # a FIFO queue of stacks to check for errors + self._keys_to_check = None # a FIFO queue of setting keys to check for errors + + self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new + # error check needs to take place while there is already one running at the moment. + self._check_in_progress = False # Whether there is an error check running in progress at the moment. + + self._application = Application.getInstance() + self._machine_manager = self._application.getMachineManager() + + # This timer delays the starting of error check so we can react less frequently if the user is frequently + # changing settings. + self._error_check_timer = QTimer(self) + self._error_check_timer.setInterval(300) + self._error_check_timer.setSingleShot(True) + + def initialize(self): + self._error_check_timer.timeout.connect(self._rescheduleCheck) + + # Reconnect all signals when the active machine gets changed. + self._machine_manager.globalContainerChanged.connect(self._onMachineChanged) + + # Whenever the machine settings get changed, we schedule an error check. + self._machine_manager.globalContainerChanged.connect(self.startErrorCheck) + self._machine_manager.globalValueChanged.connect(self.startErrorCheck) + + self._onMachineChanged() + + def _onMachineChanged(self): + if self._global_stack: + self._global_stack.propertyChanged.disconnect(self.startErrorCheck) + self._global_stack.containersChanged.disconnect(self.startErrorCheck) + + for extruder in self._global_stack.extruders.values(): + extruder.propertyChanged.disconnect(self.startErrorCheck) + extruder.containersChanged.disconnect(self.startErrorCheck) + + self._global_stack = self._machine_manager.activeMachine + + if self._global_stack: + self._global_stack.propertyChanged.connect(self.startErrorCheck) + self._global_stack.containersChanged.connect(self.startErrorCheck) + + for extruder in self._global_stack.extruders.values(): + extruder.propertyChanged.connect(self.startErrorCheck) + extruder.containersChanged.connect(self.startErrorCheck) + + hasErrorUpdated = pyqtSignal() + needToWaitForResultChanged = pyqtSignal() + errorCheckFinished = pyqtSignal() + + @pyqtProperty(bool, notify = hasErrorUpdated) + def hasError(self) -> bool: + return self._has_errors + + @pyqtProperty(bool, notify = needToWaitForResultChanged) + def needToWaitForResult(self) -> bool: + return self._need_to_check or self._check_in_progress + + # Starts the error check timer to schedule a new error check. + def startErrorCheck(self, *args): + if not self._check_in_progress: + self._need_to_check = True + self.needToWaitForResultChanged.emit() + self._error_check_timer.start() + + # This function is called by the timer to reschedule a new error check. + # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag + # to notify the current check to stop and start a new one. + def _rescheduleCheck(self): + if self._check_in_progress and not self._need_to_check: + self._need_to_check = True + self.needToWaitForResultChanged.emit() + return + + self._error_keys_in_progress = set() + self._need_to_check = False + self.needToWaitForResultChanged.emit() + + global_stack = self._machine_manager.activeMachine + if global_stack is None: + Logger.log("i", "No active machine, nothing to check.") + return + + self._stacks_to_check = deque([global_stack] + list(global_stack.extruders.values())) + self._keys_to_check = deque(global_stack.getAllKeys()) + + self._application.callLater(self._checkStack) + Logger.log("d", "New error check scheduled.") + + def _checkStack(self): + from UM.Settings.SettingDefinition import SettingDefinition + from UM.Settings.Validator import ValidatorState + + if self._need_to_check: + Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.") + self._check_in_progress = False + self._application.callLater(self.startErrorCheck) + return + + self._check_in_progress = True + + # If there is nothing to check any more, it means there is no error. + if not self._stacks_to_check or not self._keys_to_check: + # Finish + self._setResult(False) + return + + stack = self._stacks_to_check[0] + key = self._keys_to_check.popleft() + + # If there is no key left in this stack, check the next stack later. + if not self._keys_to_check: + if len(self._stacks_to_check) == 1: + stacks = None + keys = None + else: + stack = self._stacks_to_check.popleft() + self._keys_to_check = deque(stack.getAllKeys()) + + enabled = stack.getProperty(key, "enabled") + if not enabled: + self._application.callLater(self._checkStack) + return + + validation_state = stack.getProperty(key, "validationState") + if validation_state is None: + # Setting is not validated. This can happen if there is only a setting definition. + # We do need to validate it, because a setting definitions value can be set by a function, which could + # be an invalid setting. + definition = stack.getSettingDefinition(key) + validator_type = SettingDefinition.getValidatorForType(definition.type) + if validator_type: + validator = validator_type(key) + validation_state = validator(stack) + if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): + # Finish + self._setResult(True) + return + + # Schedule the check for the next key + self._application.callLater(self._checkStack) + + def _setResult(self, result: bool): + if result != self._has_errors: + self._has_errors = result + self.hasErrorUpdated.emit() + self._machine_manager.stacksValidationChanged.emit() + self._need_to_check = False + self._check_in_progress = False + self.needToWaitForResultChanged.emit() + self.errorCheckFinished.emit() + Logger.log("i", "Error check finished, result = %s", result) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index eb720000bf..f543910447 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -4,7 +4,7 @@ import collections import time #Type hinting. -from typing import Union, List, Dict, TYPE_CHECKING, Optional +from typing import List, Dict, TYPE_CHECKING, Optional from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Signal import Signal @@ -20,7 +20,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique @@ -56,11 +55,6 @@ class MachineManager(QObject): self.machine_extruder_material_update_dict = collections.defaultdict(list) - self._error_check_timer = QTimer() - self._error_check_timer.setInterval(250) - self._error_check_timer.setSingleShot(True) - self._error_check_timer.timeout.connect(self._updateStacksHaveErrors) - self._instance_container_timer = QTimer() self._instance_container_timer.setInterval(250) self._instance_container_timer.setSingleShot(True) @@ -228,15 +222,6 @@ class MachineManager(QObject): del self.machine_extruder_material_update_dict[self._global_container_stack.getId()] self.activeQualityGroupChanged.emit() - self._error_check_timer.start() - - ## Update self._stacks_valid according to _checkStacksForErrors and emit if change. - def _updateStacksHaveErrors(self) -> None: - old_stacks_have_errors = self._stacks_have_errors - self._stacks_have_errors = self._checkStacksHaveErrors() - if old_stacks_have_errors != self._stacks_have_errors: - self.stacksValidationChanged.emit() - Application.getInstance().stacksValidationFinished.emit() def _onActiveExtruderStackChanged(self) -> None: self.blurSettings.emit() # Ensure no-one has focus. @@ -256,8 +241,6 @@ class MachineManager(QObject): self.rootMaterialChanged.emit() - self._error_check_timer.start() - def _onInstanceContainersChanged(self, container) -> None: self._instance_container_timer.start() @@ -266,9 +249,6 @@ class MachineManager(QObject): # Notify UI items, such as the "changed" star in profile pull down menu. self.activeStackValueChanged.emit() - elif property_name == "validationState": - self._error_check_timer.start() - ## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again def _initMachineState(self, global_stack): material_dict = {} @@ -832,9 +812,10 @@ class MachineManager(QObject): ## This will fire the propertiesChanged for all settings so they will be updated in the front-end def forceUpdateAllSettings(self): - property_names = ["value", "resolve"] - for setting_key in self._global_container_stack.getAllKeys(): - self._global_container_stack.propertiesChanged.emit(setting_key, property_names) + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + property_names = ["value", "resolve"] + for setting_key in self._global_container_stack.getAllKeys(): + self._global_container_stack.propertiesChanged.emit(setting_key, property_names) @pyqtSlot(int, bool) def setExtruderEnabled(self, position: int, enabled) -> None: diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 2f57e634e0..af6162c8d5 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -10,7 +10,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources -from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then. from UM.Platform import Platform from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Qt.Duration import DurationFormat @@ -32,6 +31,7 @@ import Arcus from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + class CuraEngineBackend(QObject, Backend): backendError = Signal() @@ -62,23 +62,26 @@ class CuraEngineBackend(QObject, Backend): default_engine_location = execpath break + self._application = Application.getInstance() + self._multi_build_plate_model = None + self._machine_error_checker = None + if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") - Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location)) + Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) Preferences.getInstance().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False - Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) - Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() + self._stored_layer_data = [] self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob - self._scene = Application.getInstance().getController().getScene() + self._scene = self._application.getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -86,20 +89,10 @@ class CuraEngineBackend(QObject, Backend): # - whenever there is a value change, we start the timer # - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the # auto-slicing timer when that error check is finished - # If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer, - # and only wait for the error check to be finished to start the auto-slicing timer again. + # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished + # to start the auto-slicing timer again. # self._global_container_stack = None - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - self._onGlobalStackChanged() - - Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) - # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash - ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) - - # A flag indicating if an error check was scheduled - # If so, we will stop the auto-slice timer and start upon the error check - self._is_error_check_scheduled = False # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage @@ -125,13 +118,6 @@ class CuraEngineBackend(QObject, Backend): self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) - self.backendQuit.connect(self._onBackendQuit) - self.backendConnected.connect(self._onBackendConnected) - - # When a tool operation is in progress, don't slice. So we need to listen for tool operations. - Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted) - Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped) - self._slice_start_time = None Preferences.getInstance().addPreference("general/auto_slice", True) @@ -146,6 +132,30 @@ class CuraEngineBackend(QObject, Backend): self.determineAutoSlicing() Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + self._application.initializationFinished.connect(self.initialize) + + def initialize(self): + self._multi_build_plate_model = self._application.getMultiBuildPlateModel() + + self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) + self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged) + + self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._onGlobalStackChanged() + + # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash + ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) + + self.backendQuit.connect(self._onBackendQuit) + self.backendConnected.connect(self._onBackendConnected) + + # When a tool operation is in progress, don't slice. So we need to listen for tool operations. + self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted) + self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped) + + self._machine_error_checker = self._application.getMachineErrorChecker() + self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) + ## Terminate the engine process. # # This function should terminate the engine process. @@ -531,11 +541,9 @@ class CuraEngineBackend(QObject, Backend): elif property == "validationState": if self._use_timer: - self._is_error_check_scheduled = True self._change_timer.stop() def _onStackErrorCheckFinished(self): - self._is_error_check_scheduled = False if not self._slicing and self._build_plates_to_be_sliced: self.needsSlicing() self._onChanged() @@ -561,12 +569,15 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) - # testing def _invokeSlice(self): if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual - if self._is_error_check_scheduled: + if self._machine_error_checker is None: + self._change_timer.stop() + return + + if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() @@ -632,7 +643,11 @@ class CuraEngineBackend(QObject, Backend): if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual - if self._is_error_check_scheduled: + if self._machine_error_checker is None: + self._change_timer.stop() + return + + if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() @@ -786,7 +801,7 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.start() def _extruderChanged(self): - for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): + for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) self._invokeSlice()