diff --git a/.github/ISSUE_TEMPLATE/bugreport.yaml b/.github/ISSUE_TEMPLATE/bugreport.yaml index 35316a2d0b..18ce1e63e4 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yaml +++ b/.github/ISSUE_TEMPLATE/bugreport.yaml @@ -5,17 +5,17 @@ body: - type: markdown attributes: value: | - **Thank you for using Cura and wanting to report a bug.** + **Thank you for using Cura and wanting to report a bug. 🙏** - Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. + Before filing, [please check if the issue already exists](https://github.com/Ultimaker/Cura/issues?q=is%3Aissue) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment. - Also, please note the application version in the title of the issue "For example (5.3.1) Cannot connect to 3rd-party printer". Please do not write things like **Request** or **BUG** in the title, this is what labels are for. + Please include the cura version in the title of the issue. For example, *"[5.4.0] Support Brim is missing in this model"*. - type: input attributes: - label: Application Version + label: Cura Version description: The version of Cura this issue occurs with. - placeholder: 5.3.0 + placeholder: 5.4.0 validations: required: true - type: input @@ -28,14 +28,14 @@ body: - type: input attributes: label: Printer - description: Which printer was selected in Cura? - placeholder: Ultimaker S7 + description: Which printer was selected in Cura? It also helps to mention if you made any firmware modifications to your printer. + placeholder: Ultimaker S7 / Creality CR-10 with Klipper validations: required: true - type: textarea attributes: label: Reproduction steps - description: Tell us what you did! + description: Share what you did, so we can reproduce it placeholder: | 1. Something you did 2. Something you did next @@ -44,42 +44,39 @@ body: - type: textarea attributes: label: Actual results - description: What happens after the above steps have been followed. + description: What happens after the above steps have been followed? validations: required: true - type: textarea attributes: label: Expected results - description: What should happen after the above steps have been followed. + description: What should happen after the above steps have been followed? validations: required: true - type: markdown attributes: value: | - Please be sure to add the following files: - * To save a project file go to File -> Save project. - Please make sure to .zip your project file. - For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. - G-code files are not project files! - Before you share, please think to yourself. Is this a model that can be shared? - * **Screenshots** of showing the problem, perhaps before/after images. - * A **log file** for crashes and similar issues. - You can find your log file here: - Windows: `%APPDATA%\cura\\cura.log` or usually `C:\Users\\\AppData\Roaming\cura\\cura.log` - MacOS: `$USER/Library/Application Support/cura//cura.log` - Ubuntu/Linux: `$USER/.local/share/cura//cura.log` - - If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder -- type: checkboxes - attributes: - label: Checklist of files to include - options: - - label: Log file - - label: Project file + ### Please add the following files when they are related to... + * 🔵 **The quality of your print** + Please add **a Project File**. It contains the printer and settings we need for troubleshooting. + To save a project file go to File -> Save project. + Please make sure to .zip your project file. For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. + G-code files are not project files! Before you share, please think to yourself. Is this a model that can be shared? + ![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif) + * 🔵 **Using and interacting with Cura** + Please add **screenshots** showing the issue. + Before and after, and arrows can help here. + * 🔵 **Unexpected crashes and behavior** + Please add **a log file** with information on what your Cura is doing. + You can find your log file here: + Windows: `%APPDATA%\cura\\cura.log` + MacOS: `$USER/Library/Application Support/cura//cura.log` + Ubuntu/Linux: `$USER/.local/share/cura//cura.log` + If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder + - type: textarea attributes: - label: Additional information & file uploads - description: You can add these files and additional information that is relevant to the issue in the comments below. + label: Add your .zip and screenshots here ⬇️ + description: You can add the zip file and additional information that is relevant to the issue in the comments below. validations: required: true - diff --git a/.github/workflows/update-translation.yml b/.github/workflows/update-translation.yml index c1f0e027b8..65693be937 100644 --- a/.github/workflows/update-translation.yml +++ b/.github/workflows/update-translation.yml @@ -17,6 +17,13 @@ on: - 'conandata.yml' - 'GitVersion.yml' - '*.jinja' + branches: + - '[1-9].[0-9]' + - '[1-9].[0-9][0-9]' + tags: + - '[1-9].[0-9].[0-9]*' + - '[1-9].[0-9].[0-9]' + - '[1-9].[0-9][0-9].[0-9]*' jobs: update-translations: diff --git a/cura/BackendPlugin.py b/cura/BackendPlugin.py new file mode 100644 index 0000000000..de7b3f29dc --- /dev/null +++ b/cura/BackendPlugin.py @@ -0,0 +1,81 @@ +# Copyright (c) 2023 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import subprocess +from typing import Optional, List + +from UM.Logger import Logger +from UM.PluginObject import PluginObject + + +class BackendPlugin(PluginObject): + def __init__(self) -> None: + super().__init__() + self.__port: int = 0 + self._plugin_address: str = "127.0.0.1" + self._plugin_command: Optional[List[str]] = None + self._process = None + self._is_running = False + self._supported_slots: List[int] = [] + + def getSupportedSlots(self) -> List[int]: + return self._supported_slots + + def isRunning(self): + return self._is_running + + def setPort(self, port: int) -> None: + self.__port = port + + def getPort(self) -> int: + return self.__port + + def getAddress(self) -> str: + return self._plugin_address + + def _validatePluginCommand(self) -> list[str]: + """ + Validate the plugin command and add the port parameter if it is missing. + + :return: A list of strings containing the validated plugin command. + """ + if not self._plugin_command or "--port" in self._plugin_command: + return self._plugin_command or [] + + return self._plugin_command + ["--port", str(self.__port)] + + def start(self) -> bool: + """ + Starts the backend_plugin process. + + :return: True if the plugin process started successfully, False otherwise. + """ + try: + # STDIN needs to be None because we provide no input, but communicate via a local socket instead. + # The NUL device sometimes doesn't exist on some computers. + self._process = subprocess.Popen(self._validatePluginCommand(), stdin = None) + self._is_running = True + return True + except PermissionError: + Logger.log("e", f"Couldn't start backend_plugin [{self._plugin_id}]: No permission to execute process.") + except FileNotFoundError: + Logger.logException("e", f"Unable to find backend_plugin executable [{self._plugin_id}]") + except BlockingIOError: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Resource is temporarily unavailable") + except OSError as e: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Operating system is blocking it (antivirus?)") + return False + + def stop(self) -> bool: + if not self._process: + self._is_running = False + return True # Nothing to stop + + try: + self._process.terminate() + return_code = self._process.wait() + self._is_running = False + Logger.log("d", f"Backend_plugin [{self._plugin_id}] was killed. Received return code {return_code}") + return True + except PermissionError: + Logger.log("e", "Unable to kill running engine. Access is denied.") + return False diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6b04503ebc..c96de7e50a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -205,6 +205,8 @@ class CuraApplication(QtApplication): self._cura_scene_controller = None self._machine_error_checker = None + self._backend_plugins: List[BackendPlugin] = [] + self._machine_settings_manager = MachineSettingsManager(self, parent = self) self._material_management_model = None self._quality_management_model = None @@ -792,6 +794,7 @@ class CuraApplication(QtApplication): self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_writer", self._addProfileWriter) + self._plugin_registry.addType("backend_plugin", self._addBackendPlugin) if Platform.isLinux(): lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions. @@ -1730,6 +1733,12 @@ class CuraApplication(QtApplication): def _addProfileWriter(self, profile_writer): pass + def _addBackendPlugin(self, backend_plugin: "BackendPlugin") -> None: + self._backend_plugins.append(backend_plugin) + + def getBackendPlugins(self) -> List["BackendPlugin"]: + return self._backend_plugins + @pyqtSlot("QSize") def setMinimumWindowSize(self, size): main_window = self.getMainWindow() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index f5d701f6f7..ef073e6865 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -46,6 +46,19 @@ catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): backendError = Signal() + printDurationMessage = Signal() + """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.""" + + slicingCancelled = Signal() + """Emitted when the slicing process is aborted forcefully.""" + def __init__(self) -> None: """Starts the back-end plug-in. @@ -70,7 +83,7 @@ class CuraEngineBackend(QObject, Backend): os.path.join(CuraApplication.getInstallPrefix(), "bin"), os.path.dirname(os.path.abspath(sys.executable)), ] - + self._last_backend_plugin_port = self._port + 1000 for path in search_path: engine_path = os.path.join(path, executable_name) if os.path.isfile(engine_path): @@ -86,9 +99,9 @@ class CuraEngineBackend(QObject, Backend): self._default_engine_location = execpath break - application = CuraApplication.getInstance() #type: CuraApplication - self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] - self._machine_error_checker = None #type: Optional[MachineErrorChecker] + application: CuraApplication = CuraApplication.getInstance() + self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None + self._machine_error_checker: Optional[MachineErrorChecker] = None if not self._default_engine_location: raise EnvironmentError("Could not find CuraEngine") @@ -99,13 +112,15 @@ class CuraEngineBackend(QObject, Backend): application.getPreferences().addPreference("backend/location", self._default_engine_location) # Workaround to disable layer view processing if layer view is not active. - self._layer_view_active = False #type: bool + self._layer_view_active: bool = False self._onActiveViewChanged() - self._stored_layer_data = [] # type: List[Arcus.PythonMessage] - self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob + self._stored_layer_data: List[Arcus.PythonMessage] = [] - self._scene = application.getController().getScene() #type: Scene + # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob + self._stored_optimized_layer_data: Dict[int, List[Arcus.PythonMessage]] = {} + + self._scene: Scene = application.getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -116,7 +131,7 @@ class CuraEngineBackend(QObject, Backend): # 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 #type: Optional[ContainerStack] + self._global_container_stack: Optional[ContainerStack] = None # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage @@ -128,31 +143,34 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates 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[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 - self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. - self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers. - self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing? - self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not? + self._start_slice_job: Optional[StartSliceJob] = None + self._start_slice_job_build_plate: Optional[int] = None + self._slicing: bool = False # Are we currently slicing? + self._restart: bool = False # Back-end is currently restarting? + self._tool_active: bool = False # If a tool is active, some tasks do not have to do anything + self._always_restart: bool = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. + self._process_layers_job: Optional[ProcessSlicedLayersJob] = None # The currently active job to process layers, or None if it is not processing layers. + self._build_plates_to_be_sliced: List[int] = [] # what needs slicing? + self._engine_is_fresh: bool = True # Is the newly started engine used before or not? - self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer - self._error_message = None #type: Optional[Message] # Pop-up message that shows errors. - self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed - self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool) + self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer + self._error_message: Optional[Message] = None # Pop-up message that shows errors. - self._time_start_process = None #type: Optional[float] - self._is_disabled = False #type: bool + # Count number of objects to see if there is something changed + self._last_num_objects: Dict[int, int] = defaultdict(int) + self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool) + + self._time_start_process: Optional[float] = None + self._is_disabled: bool = False application.getPreferences().addPreference("general/auto_slice", False) - self._use_timer = False #type: bool - # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. - # This timer will group them up, and only slice for the last setting changed signal. + self._use_timer: bool = False + + # When you update a setting and other settings get changed through inheritance, many propertyChanged + # signals are fired. This timer will group them up, and only slice for the last setting changed signal. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. - self._change_timer = QTimer() #type: QTimer + self._change_timer: QTimer = QTimer() self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() @@ -172,10 +190,25 @@ class CuraEngineBackend(QObject, Backend): self._slicing_error_message.actionTriggered.connect(self._reportBackendError) self._resetLastSliceTimeStats() - self._snapshot = None #type: Optional[QImage] + self._snapshot: Optional[QImage] = None application.initializationFinished.connect(self.initialize) + def startPlugins(self) -> None: + """ + Ensure that all backend plugins are started + It assigns unique ports to each plugin to avoid conflicts. + :return: + """ + backend_plugins = CuraApplication.getInstance().getBackendPlugins() + for backend_plugin in backend_plugins: + if backend_plugin.isRunning(): + continue + # Set the port to prevent plugins from using the same one. + backend_plugin.setPort(self._last_backend_plugin_port) + self._last_backend_plugin_port += 1 + backend_plugin.start() + def _resetLastSliceTimeStats(self) -> None: self._time_start_process = None self._time_send_message = None @@ -202,7 +235,8 @@ class CuraEngineBackend(QObject, Backend): application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() - # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash + # 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) @@ -239,26 +273,14 @@ class CuraEngineBackend(QObject, Backend): command += ["connect", "127.0.0.1:{0}".format(self._port), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) - parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") + parser.add_argument("--debug", action = "store_true", default = False, + help = "Turn on the debug mode by setting this option.") known_args = vars(parser.parse_known_args()[0]) if known_args["debug"]: command.append("-vvv") return command - printDurationMessage = Signal() - """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.""" - - slicingCancelled = Signal() - """Emitted when the slicing process is aborted forcefully.""" - @pyqtSlot() def stopSlicing(self) -> None: self.setState(BackendState.NotStarted) @@ -266,7 +288,8 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - if self._process_layers_job is not None: # 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("i", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None @@ -281,7 +304,7 @@ class CuraEngineBackend(QObject, Backend): self.markSliceAll() self.slice() - @call_on_qt_thread # must be called from the main thread because of OpenGL + @call_on_qt_thread # Must be called from the main thread because of OpenGL def _createSnapshot(self) -> None: self._snapshot = None if not CuraApplication.getInstance().isVisible: @@ -290,7 +313,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Creating thumbnail image (just before slice)...") try: self._snapshot = Snapshot.snapshot(width = 300, height = 300) - except: + except Exception: Logger.logException("w", "Failed to create snapshot image") self._snapshot = None # Failing to create thumbnail should not fail creation of UFP @@ -302,6 +325,8 @@ class CuraEngineBackend(QObject, Backend): self._createSnapshot() + self.startPlugins() + Logger.log("i", "Starting to slice...") self._time_start_process = time() if not self._build_plates_to_be_sliced: @@ -315,7 +340,8 @@ class CuraEngineBackend(QObject, Backend): return if not hasattr(self._scene, "gcode_dict"): - self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. + self._scene.gcode_dict = {} # type: ignore + # We need to ignore type because we are creating the missing attribute here. # see if we really have to slice application = CuraApplication.getInstance() @@ -326,9 +352,9 @@ class CuraEngineBackend(QObject, Backend): self._stored_layer_data = [] - 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] = [] #type: ignore #Because we created this attribute above. + self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore + # We need to ignore the type 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() @@ -337,7 +363,7 @@ class CuraEngineBackend(QObject, Backend): if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) - if self._process is None: # type: ignore + if self._process is None: # type: ignore self._createSocket() self.stopSlicing() self._engine_is_fresh = False # Yes we're going to use the engine @@ -345,7 +371,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] 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() @@ -377,14 +403,15 @@ class CuraEngineBackend(QObject, Backend): if CuraApplication.getInstance().getUseExternalBackend(): return - if self._process is not None: # type: ignore + if self._process is not None: # type: ignore Logger.log("d", "Killing engine process") try: - self._process.terminate() # type: ignore - Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore - self._process = None # type: ignore + self._process.terminate() # type: ignore + Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore + self._process = None # type: ignore - except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. + 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)) def _onStartSliceCompleted(self, job: StartSliceJob) -> None: @@ -429,14 +456,14 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return extruders = ExtruderManager.getInstance().getActiveExtruderStacks() - error_keys = [] #type: List[str] + error_keys: List[str] = [] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: error_keys = self._global_container_stack.getErrorKeys() 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. + 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 = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key) if definitions: break #Found it! No need to continue search. @@ -524,7 +551,7 @@ class CuraEngineBackend(QObject, Backend): # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) - # Notify the user that it's now up to the backend to do it's job + # Notify the user that it's now up to the backend to do its job self.setState(BackendState.Processing) # Handle time reporting. @@ -551,7 +578,8 @@ class CuraEngineBackend(QObject, Backend): self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically. + self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list # type: ignore + # We need to ignore type because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer @@ -566,7 +594,7 @@ class CuraEngineBackend(QObject, Backend): def _numObjectsPerBuildPlate(self) -> Dict[int, int]: """Return a dict with number of objects per build plate""" - num_objects = defaultdict(int) #type: Dict[int, int] + num_objects: Dict[int, int] = defaultdict(int) for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects if node.callDecoration("isSliceable"): @@ -646,11 +674,13 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]: + if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, + Arcus.ErrorCode.ConnectionResetError, + Arcus.ErrorCode.Debug]: Logger.log("w", "A socket error caused the connection to be reset") # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status - # needs to be updated. Otherwise backendState is "Unable To Slice" + # needs to be updated. Otherwise, backendState is "Unable To Slice" if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: self._start_slice_job.setIsCancelled(False) @@ -672,7 +702,7 @@ class CuraEngineBackend(QObject, Backend): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: - # We can assume that all nodes have a parent as we're looping through the scene (and filter out root) + # We can assume that all nodes have a parent as we're looping through the scene and filter out root cast(SceneNode, node.getParent()).removeChild(node) def markSliceAll(self) -> None: @@ -701,7 +731,7 @@ class CuraEngineBackend(QObject, Backend): :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. + if property == "value": # Only re-slice if the value has changed. self.needsSlicing() self._onChanged() @@ -770,8 +800,10 @@ class CuraEngineBackend(QObject, Backend): self._time_end_slice = time() try: - gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #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. + gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore + # We need to ignore the type because it was generated dynamically. + except KeyError: + # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. gcode_list = [] application = CuraApplication.getInstance() for index, line in enumerate(gcode_list): @@ -816,7 +848,8 @@ class CuraEngineBackend(QObject, Backend): 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. + 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. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: @@ -828,7 +861,8 @@ class CuraEngineBackend(QObject, Backend): 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. + 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. def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None: @@ -955,7 +989,8 @@ class CuraEngineBackend(QObject, Backend): view = CuraApplication.getInstance().getController().getActiveView() if view: active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. + if view.getPluginId() == "SimulationView": + # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. @@ -1007,7 +1042,8 @@ class CuraEngineBackend(QObject, Backend): self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine if self._global_container_stack: - self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. + # Note: Only starts slicing when the value changed. + self._global_container_stack.propertyChanged.connect(self._onSettingChanged) self._global_container_stack.containersChanged.connect(self._onChanged) for extruder in self._global_container_stack.extruderList: diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 4f320a3a79..b1f92b3b25 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -302,21 +302,18 @@ class StartSliceJob(Job): for extruder_stack in global_stack.extruderList: self._buildExtruderMessage(extruder_stack) - # EnginePlugins - # TODO: don't hardcode them - # Ports: are chosen based on https://stackoverflow.com/questions/10476987/best-tcp-port-number-range-for-internal-applications - - plugins = { - 0: {"address": os.environ.get("SIMPLIFY_ADDRESS", "localhost"), "port": os.environ.get("SIMPLIFY_PORT", 33700)} if os.environ.get("SIMPLIFY_ENABLE") is not None else None, - 1: {"address": os.environ.get("POSTPROCESS_ADDRESS", "localhost"), "port": os.environ.get("POSTPROCESS_PORT", 33701)} if os.environ.get("POSTPROCESS_ENABLE") is not None else None, - } - - for plugin, connection in plugins.items(): - plugin_message = self._slice_message.addRepeatedMessage("engine_plugins") - plugin_message.id = plugin - if connection: - plugin_message.address = connection["address"] - plugin_message.port = connection["port"] + for plugin in CuraApplication.getInstance().getBackendPlugins(): + for slot in plugin.getSupportedSlots(): + # Right now we just send the message for every slot that we support. A single plugin can support + # multiple slots + # In the future the frontend will need to decide what slots that a plugin actually supports should + # also be used. For instance, if you have two plugins and each of them support a_generate and b_generate + # only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does + # b_generate, etc). + plugin_message = self._slice_message.addRepeatedMessage("engine_plugins") + plugin_message.id = slot + plugin_message.address = plugin.getAddress() + plugin_message.port = plugin.getPort() for group in filtered_object_groups: group_message = self._slice_message.addRepeatedMessage("object_lists")