diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 188de1b388..23175d24a7 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -6,7 +6,7 @@ import os from PyQt5.QtCore import QObject, QTimer, pyqtSlot import sys from time import time -from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING +from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING from UM.Backend.Backend import Backend, BackendState from UM.Scene.SceneNode import SceneNode @@ -18,13 +18,14 @@ from UM.Resources import Resources from UM.Platform import Platform from UM.Qt.Duration import DurationFormat from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Settings.DefinitionContainerInterface import DefinitionContainerInterface from UM.Settings.SettingInstance import SettingInstance #For typing. from UM.Tool import Tool #For typing. from cura.CuraApplication import CuraApplication from cura.Settings.ExtruderManager import ExtruderManager -from . import ProcessSlicedLayersJob -from . import StartSliceJob +from .ProcessSlicedLayersJob import ProcessSlicedLayersJob +from .StartSliceJob import StartSliceJob, StartJobResult import Arcus @@ -46,9 +47,8 @@ class CuraEngineBackend(QObject, Backend): # This registers all the signal listeners and prepares for communication # with the back-end in general. # CuraEngineBackend is exposed to qml as well. - def __init__(self, parent = None) -> None: - super(QObject, self).__init__(parent = parent) - super(Backend, self).__init__() + def __init__(self) -> None: + super().__init__() # Find out where the engine is located, and how it is called. # This depends on how Cura is packaged and which OS we are running on. executable_name = "CuraEngine" @@ -110,7 +110,7 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._start_slice_job = None #type: Optional[StartSliceJob] - self._start_slice_job_build_plate = None #type: Optional[StartSliceJob] + self._start_slice_job_build_plate = None #type: Optional[int] self._slicing = False #type: bool # Are we currently slicing? self._restart = False #type: bool # Back-end is currently restarting? self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything @@ -197,7 +197,7 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon. + if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon. Logger.log("d", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None @@ -225,7 +225,7 @@ class CuraEngineBackend(QObject, Backend): return if not hasattr(self._scene, "gcode_dict"): - self._scene.gcode_dict = {} + self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. # see if we really have to slice active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate @@ -237,7 +237,7 @@ class CuraEngineBackend(QObject, Backend): self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: - self._scene.gcode_dict[build_plate_to_be_sliced] = [] + self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above. Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: self.slice() @@ -254,14 +254,14 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number + self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() self.determineAutoSlicing() # Switch timer on or off if appropriate slice_message = self._socket.createMessage("cura.proto.Slice") - self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) + self._start_slice_job = StartSliceJob(slice_message) self._start_slice_job_build_plate = build_plate_to_be_sliced self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate) self._start_slice_job.start() @@ -310,12 +310,12 @@ class CuraEngineBackend(QObject, Backend): if self._start_slice_job is job: self._start_slice_job = None - if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: + if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error: self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: + if job.getResult() == StartJobResult.MaterialIncompatible: if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice")) @@ -326,10 +326,10 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.NotStarted) return - if job.getResult() == StartSliceJob.StartJobResult.SettingError: + if job.getResult() == StartJobResult.SettingError: if self._application.platformActivity: extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) - error_keys = [] + error_keys = [] #type: List[str] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: @@ -337,7 +337,7 @@ class CuraEngineBackend(QObject, Backend): error_labels = set() for key in error_keys: for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack. - definitions = stack.getBottom().findDefinitions(key = key) + definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key) if definitions: break #Found it! No need to continue search. else: #No stack has a definition for this setting. @@ -345,8 +345,7 @@ class CuraEngineBackend(QObject, Backend): continue error_labels.add(definitions[0].label) - error_labels = ", ".join(error_labels) - self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels), + self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) @@ -355,28 +354,27 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.NotStarted) return - elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError: + elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} - for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): + for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. stack = node.callDecoration("getStack") if not stack: continue for key in stack.getErrorKeys(): - definition = self._global_container_stack.getBottom().findDefinitions(key = key) + definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom()).findDefinitions(key = key) if not definition: Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key)) continue definition = definition[0] errors[key] = definition.label - error_labels = ", ".join(errors.values()) - self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels), + self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: + if job.getResult() == StartJobResult.BuildPlateError: if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."), title = catalog.i18nc("@info:title", "Unable to slice")) @@ -386,7 +384,7 @@ class CuraEngineBackend(QObject, Backend): else: self.backendStateChange.emit(BackendState.NotStarted) - if job.getResult() == StartSliceJob.StartJobResult.ObjectsWithDisabledExtruder: + if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -394,7 +392,7 @@ class CuraEngineBackend(QObject, Backend): self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: + if job.getResult() == StartJobResult.NothingToSlice: if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), title = catalog.i18nc("@info:title", "Unable to slice")) @@ -424,14 +422,14 @@ class CuraEngineBackend(QObject, Backend): if not self._application.getPreferences().getValue("general/auto_slice"): enable_timer = False - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isBlockSlicing"): enable_timer = False self.backendStateChange.emit(BackendState.Disabled) self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list + self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer @@ -445,8 +443,8 @@ class CuraEngineBackend(QObject, Backend): ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: - num_objects = defaultdict(int) - for node in DepthFirstIterator(self._scene.getRoot()): + num_objects = defaultdict(int) #type: Dict[int, int] + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. # Only count sliceable objects if node.callDecoration("isSliceable"): build_plate_number = node.callDecoration("getBuildPlateNumber") @@ -529,7 +527,7 @@ class CuraEngineBackend(QObject, Backend): ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("getLayerData"): if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: node.getParent().removeChild(node) @@ -611,7 +609,7 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) - gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] + gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. for index, line in enumerate(gcode_list): replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths)) @@ -649,18 +647,20 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: - self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) + self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: - self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) + self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. ## Creates a new socket connection. - def _createSocket(self) -> None: - super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto"))) + def _createSocket(self, protocol_file: str = None) -> None: + if not protocol_file: + protocol_file = os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")) + super()._createSocket(protocol_file) self._engine_is_fresh = True ## Called when anything has changed to the stuff that needs to be sliced. @@ -745,7 +745,7 @@ class CuraEngineBackend(QObject, Backend): self._onSceneChanged(source) def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None: - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) + self._process_layers_job = ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) self._process_layers_job.setBuildPlate(build_plate_number) self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start()