diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 3dd0589865..4de10c5ed0 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -42,12 +42,14 @@ catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): backendError = Signal() - ## Starts the back-end plug-in. - # - # 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) -> None: + """Starts the back-end plug-in. + + This registers all the signal listeners and prepares for communication + with the back-end in general. + CuraEngineBackend is exposed to qml as well. + """ + 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. @@ -177,18 +179,22 @@ class CuraEngineBackend(QObject, Backend): 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. - # Called when closing the application. def close(self) -> None: + """Terminate the engine process. + + This function should terminate the engine process. + Called when closing the application. + """ + # Terminate CuraEngine if it is still running at this point self._terminate() - ## Get the command that is used to call the engine. - # This is useful for debugging and used to actually start the engine. - # \return list of commands and args / parameters. def getEngineCommand(self) -> List[str]: + """Get the command that is used to call the engine. + + This is useful for debugging and used to actually start the engine. + :return: list of commands and args / parameters. + """ command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) @@ -199,17 +205,18 @@ class CuraEngineBackend(QObject, Backend): return command - ## Emitted when we get a message containing print duration and material amount. - # This also implies the slicing has finished. - # \param time The amount of time the print will take. - # \param material_amount The amount of material the print will use. printDurationMessage = Signal() - - ## Emitted when the slicing process starts. + """Emitted when we get a message containing print duration and material amount. + + This also implies the slicing has finished. + :param time: The amount of time the print will take. + :param material_amount: The amount of material the print will use. + """ slicingStarted = Signal() + """Emitted when the slicing process starts.""" - ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() + """Emitted when the slicing process is aborted forcefully.""" @pyqtSlot() def stopSlicing(self) -> None: @@ -226,14 +233,16 @@ class CuraEngineBackend(QObject, Backend): if self._error_message: self._error_message.hide() - ## Manually triggers a reslice @pyqtSlot() def forceSlice(self) -> None: + """Manually triggers a reslice""" + self.markSliceAll() self.slice() - ## Perform a slice of the scene. def slice(self) -> None: + """Perform a slice of the scene.""" + Logger.log("i", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: @@ -289,9 +298,11 @@ class CuraEngineBackend(QObject, Backend): self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) - ## Terminate the engine process. - # Start the engine process by calling _createSocket() def _terminate(self) -> None: + """Terminate the engine process. + + Start the engine process by calling _createSocket() + """ self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: @@ -316,15 +327,17 @@ class CuraEngineBackend(QObject, Backend): except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e)) - ## Event handler to call when the job to initiate the slicing process is - # completed. - # - # When the start slice job is successfully completed, it will be happily - # slicing. This function handles any errors that may occur during the - # bootstrapping of a slice job. - # - # \param job The start slice job that was just finished. def _onStartSliceCompleted(self, job: StartSliceJob) -> None: + """Event handler to call when the job to initiate the slicing process is + + completed. + + When the start slice job is successfully completed, it will be happily + slicing. This function handles any errors that may occur during the + bootstrapping of a slice job. + + :param job: The start slice job that was just finished. + """ if self._error_message: self._error_message.hide() @@ -443,11 +456,13 @@ class CuraEngineBackend(QObject, Backend): if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time ) - ## Determine enable or disable auto slicing. Return True for enable timer and False otherwise. - # It disables when - # - preference auto slice is off - # - decorator isBlockSlicing is found (used in g-code reader) def determineAutoSlicing(self) -> bool: + """Determine enable or disable auto slicing. Return True for enable timer and False otherwise. + + It disables when: + - preference auto slice is off + - decorator isBlockSlicing is found (used in g-code reader) + """ enable_timer = True self._is_disabled = False @@ -472,8 +487,9 @@ class CuraEngineBackend(QObject, Backend): self.disableTimer() return False - ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: + """Return a dict with number of objects per build plate""" + num_objects = defaultdict(int) #type: Dict[int, int] for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects @@ -483,12 +499,14 @@ class CuraEngineBackend(QObject, Backend): num_objects[build_plate_number] += 1 return num_objects - ## Listener for when the scene has changed. - # - # This should start a slice if the scene is now ready to slice. - # - # \param source The scene node that was changed. def _onSceneChanged(self, source: SceneNode) -> None: + """Listener for when the scene has changed. + + This should start a slice if the scene is now ready to slice. + + :param source: The scene node that was changed. + """ + if not source.callDecoration("isSliceable"): return @@ -536,10 +554,12 @@ class CuraEngineBackend(QObject, Backend): self._invokeSlice() - ## Called when an error occurs in the socket connection towards the engine. - # - # \param error The exception that occurred. def _onSocketError(self, error: Arcus.Error) -> None: + """Called when an error occurs in the socket connection towards the engine. + + :param error: The exception that occurred. + """ + if self._application.isShuttingDown(): return @@ -567,8 +587,9 @@ class CuraEngineBackend(QObject, Backend): break return has_slicable - ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: + """Remove old layer data (if any)""" + # Clear out any old gcode self._scene.gcode_dict = {} # type: ignore @@ -583,8 +604,9 @@ class CuraEngineBackend(QObject, Backend): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) - ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self) -> None: + """Convenient function: mark everything to slice, emit state and clear layer data""" + # CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current # gcode. This can break Gcode file loading if it tries to remove it afterwards. if not self.hasSlicableObject(): @@ -597,10 +619,12 @@ class CuraEngineBackend(QObject, Backend): # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData() - ## A setting has changed, so check if we must reslice. - # \param instance The setting instance that has changed. - # \param property The property of the setting instance that has changed. def _onSettingChanged(self, instance: SettingInstance, property: str) -> None: + """A setting has changed, so check if we must reslice. + + :param instance: The setting instance that has changed. + :param property: The property of the setting instance that has changed. + """ if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() @@ -618,25 +642,31 @@ class CuraEngineBackend(QObject, Backend): self.needsSlicing() self._onChanged() - ## Called when a sliced layer data message is received from the engine. - # - # \param message The protobuf message containing sliced layer data. def _onLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + self._stored_layer_data.append(message) - ## Called when an optimized sliced layer data message is received from the engine. - # - # \param message The protobuf message containing sliced layer data. def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when an optimized sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + if self._start_slice_job_build_plate is not None: if self._start_slice_job_build_plate not in self._stored_optimized_layer_data: self._stored_optimized_layer_data[self._start_slice_job_build_plate] = [] self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message) - ## Called when a progress message is received from the engine. - # - # \param message The protobuf message containing the slicing progress. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a progress message is received from the engine. + + :param message: The protobuf message containing the slicing progress. + """ + self.processingProgress.emit(message.amount) self.setState(BackendState.Processing) @@ -653,10 +683,12 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## Called when the engine sends a message that slicing is finished. - # - # \param message The protobuf message signalling that slicing is finished. def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None: + """Called when the engine sends a message that slicing is finished. + + :param message: The protobuf message signalling that slicing is finished. + """ + self.setState(BackendState.Done) self.processingProgress.emit(1.0) @@ -698,27 +730,32 @@ class CuraEngineBackend(QObject, Backend): self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode self._invokeSlice() - ## Called when a g-code message is received from the engine. - # - # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a g-code message is received from the engine. + + :param message: The protobuf message containing g-code, encoded as UTF-8. + """ + try: 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. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. - ## 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: + """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. + """ + try: 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. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. - ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: + """Creates a new socket connection.""" + if not protocol_file: plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if not plugin_path: @@ -728,10 +765,12 @@ class CuraEngineBackend(QObject, Backend): super()._createSocket(protocol_file) self._engine_is_fresh = True - ## Called when anything has changed to the stuff that needs to be sliced. - # - # This indicates that we should probably re-slice soon. def _onChanged(self, *args: Any, **kwargs: Any) -> None: + """Called when anything has changed to the stuff that needs to be sliced. + + This indicates that we should probably re-slice soon. + """ + self.needsSlicing() if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, @@ -745,11 +784,13 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## Called when a print time message is received from the engine. - # - # \param message The protobuf message containing the print time per feature and - # material amount per extruder def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None: + """Called when a print time message is received from the engine. + + :param message: The protobuf message containing the print time per feature and + material amount per extruder + """ + material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) @@ -757,10 +798,12 @@ class CuraEngineBackend(QObject, Backend): times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) - ## Called for parsing message to retrieve estimated time per feature - # - # \param message The protobuf message containing the print time per feature def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]: + """Called for parsing message to retrieve estimated time per feature + + :param message: The protobuf message containing the print time per feature + """ + result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, @@ -777,19 +820,22 @@ class CuraEngineBackend(QObject, Backend): } return result - ## Called when the back-end connects to the front-end. def _onBackendConnected(self) -> None: + """Called when the back-end connects to the front-end.""" + if self._restart: self._restart = False self._onChanged() - ## Called when the user starts using some tool. - # - # When the user starts using a tool, we should pause slicing to prevent - # continuously slicing while the user is dragging some tool handle. - # - # \param tool The tool that the user is using. def _onToolOperationStarted(self, tool: Tool) -> None: + """Called when the user starts using some tool. + + When the user starts using a tool, we should pause slicing to prevent + continuously slicing while the user is dragging some tool handle. + + :param tool: The tool that the user is using. + """ + self._tool_active = True # Do not react on scene change self.disableTimer() # Restart engine as soon as possible, we know we want to slice afterwards @@ -797,12 +843,14 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - ## Called when the user stops using some tool. - # - # This indicates that we can safely start slicing again. - # - # \param tool The tool that the user was using. def _onToolOperationStopped(self, tool: Tool) -> None: + """Called when the user stops using some tool. + + This indicates that we can safely start slicing again. + + :param tool: The tool that the user was using. + """ + self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes @@ -816,8 +864,9 @@ class CuraEngineBackend(QObject, Backend): self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() - ## Called when the user changes the active view mode. def _onActiveViewChanged(self) -> None: + """Called when the user changes the active view mode.""" + view = self._application.getController().getActiveView() if view: active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate @@ -835,17 +884,20 @@ class CuraEngineBackend(QObject, Backend): else: self._layer_view_active = False - ## Called when the back-end self-terminates. - # - # We should reset our state and start listening for new connections. def _onBackendQuit(self) -> None: + """Called when the back-end self-terminates. + + We should reset our state and start listening for new connections. + """ + if not self._restart: if self._process: # type: ignore Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore self._process = None # type: ignore - ## Called when the global container stack changes def _onGlobalStackChanged(self) -> None: + """Called when the global container stack changes""" + if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) @@ -874,15 +926,18 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() - ## Connect slice function to timer. def enableTimer(self) -> None: + """Connect slice function to timer.""" + if not self._use_timer: self._change_timer.timeout.connect(self.slice) self._use_timer = True - ## Disconnect slice function from timer. - # This means that slicing will not be triggered automatically def disableTimer(self) -> None: + """Disconnect slice function from timer. + + This means that slicing will not be triggered automatically + """ if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) @@ -894,8 +949,9 @@ class CuraEngineBackend(QObject, Backend): if auto_slice: self._change_timer.start() - ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self) -> None: + """Tickle the backend so in case of auto slicing, it starts the timer.""" + if self._use_timer: self._change_timer.start() diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 32d60eb68b..9cd48cdc94 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -28,10 +28,12 @@ from cura.Machines.Models.ExtrudersModel import ExtrudersModel catalog = i18nCatalog("cura") -## Return a 4-tuple with floats 0-1 representing the html color code -# -# \param color_code html color code, i.e. "#FF0000" -> red def colorCodeToRGBA(color_code): + """Return a 4-tuple with floats 0-1 representing the html color code + + :param color_code: html color code, i.e. "#FF0000" -> red + """ + if color_code is None: Logger.log("w", "Unable to convert color code, returning default") return [0, 0, 0, 1] @@ -51,13 +53,15 @@ class ProcessSlicedLayersJob(Job): self._abort_requested = False self._build_plate_number = None - ## Aborts the processing of layers. - # - # This abort is made on a best-effort basis, meaning that the actual - # job thread will check once in a while to see whether an abort is - # requested and then stop processing by itself. There is no guarantee - # that the abort will stop the job any time soon or even at all. def abort(self): + """Aborts the processing of layers. + + This abort is made on a best-effort basis, meaning that the actual + job thread will check once in a while to see whether an abort is + requested and then stop processing by itself. There is no guarantee + that the abort will stop the job any time soon or even at all. + """ + self._abort_requested = True def setBuildPlate(self, new_value): diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index a99c559bac..ef9aeef122 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -40,8 +40,9 @@ class StartJobResult(IntEnum): ObjectsWithDisabledExtruder = 8 -## Formatter class that handles token expansion in start/end gcode class GcodeStartEndFormatter(Formatter): + """Formatter class that handles token expansion in start/end gcode""" + def __init__(self, default_extruder_nr: int = -1) -> None: super().__init__() self._default_extruder_nr = default_extruder_nr @@ -82,8 +83,9 @@ class GcodeStartEndFormatter(Formatter): return value -## Job class that builds up the message of scene data to send to CuraEngine. class StartSliceJob(Job): + """Job class that builds up the message of scene data to send to CuraEngine.""" + def __init__(self, slice_message: Arcus.PythonMessage) -> None: super().__init__() @@ -100,9 +102,11 @@ class StartSliceJob(Job): def setBuildPlate(self, build_plate_number: int) -> None: self._build_plate_number = build_plate_number - ## Check if a stack has any errors. - ## returns true if it has errors, false otherwise. def _checkStackForErrors(self, stack: ContainerStack) -> bool: + """Check if a stack has any errors.""" + + """returns true if it has errors, false otherwise.""" + if stack is None: return False @@ -119,8 +123,9 @@ class StartSliceJob(Job): Job.yieldThread() return False - ## Runs the job that initiates the slicing. def run(self) -> None: + """Runs the job that initiates the slicing.""" + if self._build_plate_number is None: self.setResult(StartJobResult.Error) return @@ -323,14 +328,14 @@ class StartSliceJob(Job): def setIsCancelled(self, value: bool): self._is_cancelled = value - ## Creates a dictionary of tokens to replace in g-code pieces. - # - # This indicates what should be replaced in the start and end g-codes. - # \param stack The stack to get the settings from to replace the tokens - # with. - # \return A dictionary of replacement tokens to the values they should be - # replaced with. def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: + """Creates a dictionary of tokens to replace in g-code pieces. + + This indicates what should be replaced in the start and end g-codes. + :param stack: The stack to get the settings from to replace the tokens with. + :return: A dictionary of replacement tokens to the values they should be replaced with. + """ + result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") @@ -358,10 +363,12 @@ class StartSliceJob(Job): extruder_nr = extruder_stack.getProperty("extruder_nr", "value") self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) - ## Replace setting tokens in a piece of g-code. - # \param value A piece of g-code to replace tokens in. - # \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str: + """Replace setting tokens in a piece of g-code. + + :param value: A piece of g-code to replace tokens in. + :param default_extruder_nr: Stack nr to use when no stack nr is specified, defaults to the global stack + """ if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -377,8 +384,9 @@ class StartSliceJob(Job): Logger.logException("w", "Unable to do token replacement on start/end g-code") return str(value) - ## Create extruder message from stack def _buildExtruderMessage(self, stack: ContainerStack) -> None: + """Create extruder message from stack""" + message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: @@ -407,11 +415,13 @@ class StartSliceJob(Job): setting.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends all global settings to the engine. - # - # The settings are taken from the global stack. This does not include any - # per-extruder settings or per-object settings. def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: + """Sends all global settings to the engine. + + The settings are taken from the global stack. This does not include any + per-extruder settings or per-object settings. + """ + if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -445,15 +455,16 @@ class StartSliceJob(Job): setting_message.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends for some settings which extruder they should fallback to if not - # set. - # - # This is only set for settings that have the limit_to_extruder - # property. - # - # \param stack The global stack with all settings, from which to read the - # limit_to_extruder property. def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: + """Sends for some settings which extruder they should fallback to if not set. + + This is only set for settings that have the limit_to_extruder + property. + + :param stack: The global stack with all settings, from which to read the + limit_to_extruder property. + """ + for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. @@ -462,10 +473,13 @@ class StartSliceJob(Job): setting_extruder.extruder = extruder_position Job.yieldThread() - ## Check if a node has per object settings and ensure that they are set correctly in the message - # \param node Node to check. - # \param message object_lists message to put the per object settings in def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage): + """Check if a node has per object settings and ensure that they are set correctly in the message + + :param node: Node to check. + :param message: object_lists message to put the per object settings in + """ + stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. @@ -501,10 +515,13 @@ class StartSliceJob(Job): Job.yieldThread() - ## Recursive function to put all settings that require each other for value changes in a list - # \param relations_set Set of keys of settings that are influenced - # \param relations list of relation objects that need to be checked. def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]): + """Recursive function to put all settings that require each other for value changes in a list + + :param relations_set: Set of keys of settings that are influenced + :param relations: list of relation objects that need to be checked. + """ + for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations): if relation.type == RelationType.RequiresTarget: continue