def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self._selected_printer = self._automatic_printer # reset to default option self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] if self._stage != OutputStage.ready: if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() return self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer if len(self._printers) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. self.selectAutomaticPrinter() self.sendPrintJob() else: # Cluster has no printers, warn the user of this. if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers.")) self._error_message.show()
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: # Show an error message if we're already sending a job. if self._progress.visible: message = Message( text = I18N_CATALOG.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job."), title = I18N_CATALOG.i18nc("@info:title", "Cloud error"), lifetime = 10 ) message.show() return if self._uploaded_print_job: # The mesh didn't change, let's not upload it again self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted) return # Indicate we have started sending a job. self.writeStarted.emit(self) mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) if not mesh_format.is_valid: Logger.log("e", "Missing file or mesh writer!") return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) mesh = mesh_format.getBytes(nodes) self._tool_path = mesh request = CloudPrintJobUploadRequest( job_name = file_name or mesh_format.file_extension, file_size = len(mesh), content_type = mesh_format.mime_type, ) self._api.requestUpload(request, self._onPrintJobCreated)
def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() yield #Wait on the user to select a target printer. yield #Wait for the write job to be finished. yield False #Return whether this was a success or not. yield #Prevent StopIteration. self._sending_gcode = True target_printer = yield #Potentially wait on the user to select a target printer. # Using buffering greatly reduces the write time for many lines of gcode if preferred_format["mode"] == FileWriter.OutputMode.TextMode: stream = io.StringIO() else: #Binary mode. stream = io.BytesIO() job = WriteFileJob(writer, stream, nodes, preferred_format["mode"]) self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False) self._write_job_progress_message.show() self._dummy_lambdas = (target_printer, preferred_format, stream) job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) job.start() yield True #Return that we had success! yield #To prevent having to catch the StopIteration exception.
def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() yield #Wait on the user to select a target printer. yield #Wait for the write job to be finished. yield False #Return whether this was a success or not. yield #Prevent StopIteration. self._sending_gcode = True # Potentially wait on the user to select a target printer. target_printer = yield # type: Optional[str] # Using buffering greatly reduces the write time for many lines of gcode stream = mesh_format.createStream() job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False) self._write_job_progress_message.show() if mesh_format.preferred_format is not None: self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream) job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) job.start() yield True # Return that we had success! yield # To prevent having to catch the StopIteration exception.
class BackendProxy(QObject): def __init__(self, parent = None): super().__init__(parent) self._backend = Application.getInstance().getBackend() self._progress = -1; self._messageDisplayed = False self._message = None if self._backend: self._backend.processingProgress.connect(self._onProcessingProgress) processingProgress = pyqtSignal(float, arguments = ["amount"]) @pyqtProperty(float, notify = processingProgress) def progress(self): if self._progress > 0 and self._progress < 1 and self._messageDisplayed == False: self._message = Message(i18n_catalog.i18n("Slicing in Process: "), 0, False, self._progress) self._message.show() self._messageDisplayed = True if self._progress >= 1 and self._messageDisplayed == True: self._message.hide() self._messageDisplayed = False return self._progress def _onProcessingProgress(self, amount): self._progress = amount self.processingProgress.emit(amount)
def _onStartSliceCompleted(self, job): # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: return if job.getResult() == StartSliceJob.StartJobResult.SettingError: if Application.getInstance().getPlatformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."), lifetime = 10) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if Application.getInstance().getPlatformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. No suitable objects found."), lifetime = 10) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage())
def _onStartSliceCompleted(self, job): # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: return if job.getResult() == StartSliceJob.StartJobResult.SettingError: if Application.getInstance().getPlatformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. Please check your settings for errors.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if Application.getInstance().getPlatformActivity: 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.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return # 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 self.backendStateChange.emit(BackendState.Processing) Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
def exportProfile(self, id, name, url, fileType): #Input checking. path = url.toLocalFile() if not path: error_str = "Not a valid path. If this problem persists, please report a bug." error_str = i18nCatalog.i18nc("@info:status", error_str) return {"status":"error", "message":error_str} if id == -1: #id -1 references the "Current settings"/working profile profile = copy.deepcopy(self._working_profile) profile.setType(None) profile.setMachineTypeId(self._manager.getActiveMachineInstance().getMachineDefinition().getProfilesMachineId()) else: profile = self._manager.findProfile(name, instance = self._manager.getActiveMachineInstance()) if not profile: error_str = "Profile not found. If this problem persists, please report a bug." error_str = i18nCatalog.i18nc("@info:status", error_str) return {"status":"error", "message":error_str} #Parse the fileType to deduce what plugin can save the file format. #TODO: This parsing can be made unnecessary by storing for each plugin what the fileType string is in complete (in addition to the {(description,extension)} dict). #fileType has the format "<description> (*.<extension>)" split = fileType.rfind(" (*.") #Find where the description ends and the extension starts. if split < 0: #Not found. Invalid format. Logger.log("e", "Invalid file format identifier %s", fileType) error_str = catalog.i18nc("@info:status", "Invalid file format: {0}", fileType) return {"status":"error", "message":error_str} description = fileType[:split] extension = fileType[split + 4:-1] #Leave out the " (*." and ")". if not path.endswith("." + extension): #Auto-fill the extension if the user did not provide any. path += "." + extension good_profile_writer = None for profile_writer_id, profile_writer in self._manager.getProfileWriters(): #Find which profile writer can write this file type. meta_data = PluginRegistry.getInstance().getMetaData(profile_writer_id) if "profile_writer" in meta_data: for supported_type in meta_data["profile_writer"]: #All file types this plugin can supposedly write. supported_extension = supported_type.get("extension", None) if supported_extension == extension: #This plugin supports a file type with the same extension. supported_description = supported_type.get("description", None) if supported_description == description: #The description is also identical. Assume it's the same file type. good_profile_writer = profile_writer break if good_profile_writer: #If we found a writer in this iteration, break the loop. break success = False try: success = good_profile_writer.write(path, profile) except Exception as e: Logger.log("e", "Failed to export profile to %s: %s", path, str(e)) error_str = catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", path, str(e)) return {"status":"error", "message":error_str} if not success: Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", path) error_str = catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", path) return {"status":"error", "message":error_str} m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", path)) m.show()
def run(self): loading_message = Message(i18n_catalog.i18nc("Loading mesh message, {0} is file name", "Loading {0}").format(self._filename), lifetime = 0, dismissable = False) loading_message.setProgress(-1) loading_message.show() mesh = self._handler.read(self._filename, self._device) # Scale down to maximum bounds size if that is available if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"): max_bounds = Application.getInstance().getController().getScene()._maximum_bounds bbox = mesh.getExtents() if max_bounds.width < bbox.width or max_bounds.height < bbox.height or max_bounds.depth < bbox.depth: largest_dimension = max(bbox.width, bbox.height, bbox.depth) scale_factor = 1.0 if largest_dimension == bbox.width: scale_factor = max_bounds.width / bbox.width elif largest_dimension == bbox.height: scale_factor = max_bounds.height / bbox.height else: scale_factor = max_bounds.depth / bbox.depth matrix = Matrix() matrix.setByScaleFactor(scale_factor) mesh = mesh.getTransformed(matrix) self.setResult(mesh) loading_message.hide() result_message = Message(i18n_catalog.i18nc("Finished loading mesh message, {0} is file name", "Loaded {0}").format(self._filename)) result_message.show()
def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() return self._sending_gcode = True self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, on_finished=self._onPostPrintJobFinished) return
def _onApiError(self, errors: List[CloudError] = None) -> None: Logger.log("w", str(errors)) message = Message( text = self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the cloud."), title = self.I18N_CATALOG.i18nc("@info:title", "Error"), lifetime = 10 ) message.show()
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0) status_message.show() arranger = Arrange.create(fixed_nodes = self._fixed_nodes) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() # Place nodes one at a time start_priority = 0 last_priority = start_priority last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). # We also skip possibilities by slicing through the possibilities (step = 10) if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place last_size = size last_priority = best_spot.priority arranger.place(x, y, hull_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects")) no_full_solution_message.show()
def _onActionTriggered(self, message, action): if action == "eject": if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self): message.hide() eject_message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(self.getName()), title = catalog.i18nc("@info:title", "Safely Remove Hardware")) else: eject_message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(self.getName()), title = catalog.i18nc("@info:title", "Warning")) eject_message.show()
def showMessage(self, message: Message) -> None: with self._message_lock: if message not in self._visible_messages: self._visible_messages.append(message) message.setLifetimeTimer(QTimer()) message.setInactivityTimer(QTimer()) self.visibleMessageAdded.emit(message) # also show toast message when the main window is minimized self.showToastMessage(self._app_name, message.getText())
def _showRequestFailedMessage(self, reply): if reply is not None: Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format( cluster_name = self.getName(), error_string = str(reply.errorString()), error = str(reply.error()))) error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.") message = Message(text=error_message_template.format( cluster_name = self.getName())) message.show()
def startPrint(self): if self.jobState != "ready" and self.jobState != "": self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e))
def _onStartSliceCompleted(self, job): # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: return if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if Application.getInstance().getPlatformActivity: self._error_message = Message(catalog.i18nc("@info:status", "The selected material is incompatible with the selected machine or configuration.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.SettingError: if Application.getInstance().getPlatformActivity: extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) error_keys = [] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) else: error_keys = self._global_container_stack.getErrorKeys() error_labels = set() definition_container = self._global_container_stack.getBottom() for key in error_keys: error_labels.add(definition_container.findDefinitions(key = key)[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.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if Application.getInstance().getPlatformActivity: 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.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return # 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 self.backendStateChange.emit(BackendState.Processing) Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
def requestWrite(self, node, file_name=None, filter_by_machine=False): if self._writing: raise OutputDeviceError.DeviceBusyError() file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() machine_file_formats = Application.getInstance().getMachineManager().getActiveMachineInstance().getMachineDefinition().getFileFormats() file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError() writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) extension = file_formats[0]["extension"] if file_name == None: for n in BreadthFirstIterator(node): if n.getMeshData(): file_name = n.getName() if file_name: break if not file_name: Logger.log("e", "Could not determine a proper file name when trying to print, aborting") raise OutputDeviceError.WriteRequestFailedError() temp_dir = os.path.join(tempfile.gettempdir(), "Kiddo") if not os.path.exists(temp_dir): os.mkdir(temp_dir) if extension: extension = "." + extension file_name = os.path.join(temp_dir, os.path.splitext(file_name)[0] + extension) try: Logger.log("d", "Writing to %s", file_name) stream = open(file_name, "wt") job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) message = Message(catalog.i18nc("@info:progress", "Preparing print job"), 0, False, -1) message.show() self.writeStarted.emit(self) job._message = message self._writing = True job.start() except PermissionError as e: Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e)) raise OutputDeviceError.PermissionDeniedError(e) except OSError as e: Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError(e)
def checkNewVersion(self, silent = False): no_new_version = True application_name = Application.getInstance().getApplicationName() Logger.log("i", "Checking for new version of %s" % application_name) try: latest_version_file = urllib.request.urlopen("http://software.ultimaker.com/latest.json") except Exception as e: Logger.log("e", "Failed to check for new version. %s" %e) if not silent: Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show() return try: reader = codecs.getreader("utf-8") data = json.load(reader(latest_version_file)) try: if Application.getInstance().getVersion() is not "master": local_version = Version(Application.getInstance().getVersion()) else: if not silent: Message(i18n_catalog.i18nc("@info", "The version you are using does not support checking for updates.")).show() return except ValueError: Logger.log("w", "Could not determine application version from string %s, not checking for updates", Application.getInstance().getVersion()) if not silent: Message(i18n_catalog.i18nc("@info", "The version you are using does not support checking for updates.")).show() return if application_name in data: for key, value in data[application_name].items(): if "major" in value and "minor" in value and "revision" in value and "url" in value: os = key if platform.system() == os: #TODO: add architecture check newest_version = Version([int(value["major"]), int(value["minor"]), int(value["revision"])]) if local_version < newest_version: Logger.log("i", "Found a new version of the software. Spawning message") message = Message(i18n_catalog.i18nc("@info", "A new version is available!")) message.addAction("download", i18n_catalog.i18nc("@action:button", "Download"), "[no_icon]", "[no_description]") self._url = value["url"] message.actionTriggered.connect(self.actionTriggered) message.show() no_new_version = False break else: Logger.log("e", "Could not find version information or download url for update.") else: Logger.log("e", "Did not find any version information for %s." % application_name) except Exception as e: Logger.log("e", "Exception in update checker: %s" % (e)) if no_new_version and not silent: Message(i18n_catalog.i18nc("@info", "No new version was found.")).show()
def run(self): if not self._url: Logger.log("e", "Can not check for a new release. URL not set!") return try: application_name = Application.getInstance().getApplicationName() headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())} request = urllib.request.Request(self._url, headers = headers) current_version_file = urllib.request.urlopen(request) reader = codecs.getreader("utf-8") # get machine name from the definition container machine_name = self._container.definition.getName() machine_name_parts = machine_name.lower().split(" ") # If it is not None, then we compare between the checked_version and the current_version # Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any # other Ultimaker 3 that will come in the future if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]: Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!") # Nothing to parse, just get the string # TODO: In the future may be done by parsing a JSON file with diferent version for each printer model current_version = reader(current_version_file).readline().rstrip() # If it is the first time the version is checked, the checked_version is '' checked_version = Preferences.getInstance().getValue("info/latest_checked_firmware") # If the checked_version is '', it's because is the first time we check firmware and in this case # we will not show the notification, but we will store it for the next time Preferences.getInstance().setValue("info/latest_checked_firmware", current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) # The first time we want to store the current version, the notification will not be shown, # because the new version of Cura will be release before the firmware and we don't want to # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE") message = Message(i18n_catalog.i18nc("@info Don't translate {machine_name}, since it gets replaced by a printer name!", "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(machine_name = machine_name), title = i18n_catalog.i18nc("@info:title The %s gets replaced with the printer name.", "New %s firmware available") % machine_name) message.addAction("download", i18n_catalog.i18nc("@action:button", "How to update"), "[no_icon]", "[no_description]") # If we do this in a cool way, the download url should be available in the JSON file if self._set_download_url_callback: self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update") message.actionTriggered.connect(self._callback) message.show() except Exception as e: Logger.log("w", "Failed to check for new version: %s", e) if not self.silent: Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show() return
def requestWrite(self, nodes, file_name = None, filter_by_machine = False): filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() # Formats supported by this application (File types that we can actually write) file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() if filter_by_machine: container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"}) # Create a list from supported file formats string machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")] # Take the intersection between file_formats and machine_file_formats. file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError() # Just take the first file format available. writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) extension = file_formats[0]["extension"] if file_name is None: file_name = self._automaticFileName(nodes) if extension: # Not empty string. extension = "." + extension file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension) try: Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1) message.show() self.writeStarted.emit(self) job._message = message self._writing = True job.start() except PermissionError as e: Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e)) raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e except OSError as e: Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
def _writeToDevice(self, node, device_id): device = self._device_manager.getOutputDevice(device_id) if not device: return try: device.requestWrite(node) except OutputDeviceError.UserCanceledError: pass except OutputDeviceError.WriteRequestFailedError as e: message = Message(str(e)) message.show()
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object")) status_message.show() scene = Application.getInstance().getController().getScene() total_progress = len(self._objects) * self._count current_progress = 0 root = scene.getRoot() arranger = Arrange.create(scene_root=root) nodes = [] for node in self._objects: # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() node_too_big = False if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) else: node_too_big = True found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) if node_too_big or not solution_found: found_solution_for_all = False new_location = node.getPosition() new_location = new_location.set(z = 100 - i * 20) node.setPosition(new_location) nodes.append(node) current_progress += 1 status_message.setProgress((current_progress / total_progress) * 100) Job.yieldThread() Job.yieldThread() if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object")) no_full_solution_message.show()
def requestWrite(self, node, file_name = None, filter_by_machine = False): filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() #Formats supported by this application. if filter_by_machine: machine_file_formats = Application.getInstance().getMachineManager().getActiveMachineInstance().getMachineDefinition().getFileFormats() file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) #Take the intersection between file_formats and machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError() writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) #Just take the first file format available. extension = file_formats[0]["extension"] if file_name == None: for n in BreadthFirstIterator(node): if n.getMeshData(): file_name = n.getName() if file_name: break if not file_name: Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName()) raise OutputDeviceError.WriteRequestFailedError() if extension: #Not empty string. extension = "." + extension file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension) try: Logger.log("d", "Writing to %s", file_name) stream = open(file_name, "wt") job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1) message.show() self.writeStarted.emit(self) job._message = message self._writing = True job.start() except PermissionError as e: Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e)) raise OutputDeviceError.PermissionDeniedError() from e except OSError as e: Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError() from e
def _showRequestSucceededMessage(self): confirmation_message_template = i18n_catalog.i18nc( "@info:status", "Sent {file_name} to group {cluster_name}." ) file_name = os.path.basename(self._file_name).split(".")[0] message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name) message = Message(text=message_text) button_text = i18n_catalog.i18nc("@action:button", "Show print jobs") button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.") message.addAction("open_browser", button_text, "globe", button_tooltip) message.actionTriggered.connect(self._onMessageActionTriggered) message.show()
def exportProfile(self, instance_ids, file_name, file_type): # Parse the fileType to deduce what plugin can save the file format. # fileType has the format "<description> (*.<extension>)" split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. if split < 0: # Not found. Invalid format. Logger.log("e", "Invalid file format identifier %s", file_type) return description = file_type[:split] extension = file_type[split + 4:-1] # Leave out the " (*." and ")". if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. file_name += "." + extension # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. if not Platform.isWindows(): if os.path.exists(file_name): result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name)) if result == QMessageBox.No: return found_containers = [] extruder_positions = [] for instance_id in instance_ids: containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id) if containers: found_containers.append(containers[0]) # Determine the position of the extruder of this container extruder_id = containers[0].getMetaDataEntry("extruder", "") if extruder_id == "": # Global stack extruder_positions.append(-1) else: extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id) if extruder_containers: extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0))) else: extruder_positions.append(0) # Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...) found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))] profile_writer = self._findProfileWriter(extension, description) try: success = profile_writer.write(file_name, found_containers) except Exception as e: Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0) m.show() return if not success: Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0) m.show() return m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name)) m.show()
def slice(self): self._stored_layer_data = [] if not self._enabled: #We shouldn't be slicing. return if self._slicing: #We were already slicing. Stop the old job. self._terminate() if self._process_layers_job: #We were processing layers. Stop that, the layers are going to change soon. self._process_layers_job.abort() self._process_layers_job = None #Don't slice if there is a setting with an error value. stack = Application.getInstance().getGlobalContainerStack() for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") #Only setting instances have a validation state, so settings which #are not overwritten by any instance will have none. The property #then, and only then, evaluates to None. We make the assumption that #the definition defines the setting with a default value that is #valid. Therefore we can allow both ValidatorState.Valid and None as #allowable validation states. #TODO: This assumption is wrong! If the definition defines an inheritance function that through inheritance evaluates to a disallowed value, a setting is still invalid even though it's default! #TODO: Therefore we must also validate setting definitions. if validation_state != None and validation_state != ValidatorState.Valid: Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) if self._message: #Hide any old message before creating a new one. self._message.hide() self._message = None self._message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors.")) self._message.show() return self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NOT_STARTED) if self._message: self._message.setProgress(-1) else: self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1) self._message.show() self._scene.gcode_list = [] self._slicing = True self.slicingStarted.emit() slice_message = self._socket.createMessage("cura.proto.Slice") settings_message = self._socket.createMessage("cura.proto.SettingList"); self._start_slice_job = StartSliceJob.StartSliceJob(slice_message, settings_message) self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted)
def _installPackage(self, installation_package_data: Dict[str, Any]) -> None: package_info = installation_package_data["package_info"] filename = installation_package_data["filename"] package_id = package_info["package_id"] Logger.log("i", "Installing package [%s] from file [%s]", package_id, filename) # Load the cached package file and extract all contents to a temporary directory if not os.path.exists(filename): Logger.log("w", "Package [%s] file '%s' is missing, cannot install this package", package_id, filename) return try: with zipfile.ZipFile(filename, "r") as archive: temp_dir = tempfile.TemporaryDirectory() archive.extractall(temp_dir.name) except Exception: Logger.logException("e", "Failed to install package from file [%s]", filename) return # Remove it first and then install try: self._purgePackage(package_id) except Exception as e: message = Message(catalog.i18nc("@error:update", "There was an error uninstalling the package {package} before installing " "new version:\n{error}.\nPlease try to upgrade again later.".format( package = package_id, error = str(e))), title = catalog.i18nc("@info:title", "Updating error")) message.show() return # Copy the folders there for sub_dir_name, installation_root_dir in self._installation_dirs_dict.items(): src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name) dst_dir_path = os.path.join(installation_root_dir, package_id) if not os.path.exists(src_dir_path): Logger.log("w", "The path %s does not exist, so not installing the files", src_dir_path) continue self.__installPackageFiles(package_id, src_dir_path, dst_dir_path) # Remove the file try: os.remove(filename) except Exception: Logger.log("w", "Tried to delete file [%s], but it failed", filename) # Move the info to the installed list of packages only when it succeeds self._installed_package_dict[package_id] = self._to_install_package_dict[package_id] self._installed_package_dict[package_id]["package_info"]["is_installed"] = True
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): container_stack = Application.getInstance().getGlobalContainerStack() if container_stack.getProperty("machine_gcode_flavor", "value") == "UltiGCode": self._error_message = Message(catalog.i18nc("@info:status", "This printer does not support USB printing because it uses UltiGCode flavor.")) self._error_message.show() return elif not container_stack.getMetaDataEntry("supports_usb_connection"): self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer does not support usb printing.")) self._error_message.show() return Application.getInstance().showPrintMonitor.emit(True) self.startPrint()
def run(self): super().run() if not self._result: self._result = [] # Scale down to maximum bounds size if that is available if hasattr(self._application.getController().getScene(), "_maximum_bounds"): for node in self._result: max_bounds = self._application.getController().getScene()._maximum_bounds node._resetAABB() build_bounds = node.getBoundingBox() if build_bounds is None or max_bounds is None: continue if self._application.getInstance().getPreferences().getValue("mesh/scale_to_fit") == True or self._application.getInstance().getPreferences().getValue("mesh/scale_tiny_meshes") == True: scale_factor_width = max_bounds.width / build_bounds.width scale_factor_height = max_bounds.height / build_bounds.height scale_factor_depth = max_bounds.depth / build_bounds.depth scale_factor = min(scale_factor_width, scale_factor_depth, scale_factor_height) if self._application.getInstance().getPreferences().getValue("mesh/scale_to_fit") == True and (scale_factor_width < 1 or scale_factor_height < 1 or scale_factor_depth < 1): # Use scale factor to scale large object down # Ignore scaling on models which are less than 1.25 times bigger than the build volume ignore_factor = 1.25 if 1 / scale_factor < ignore_factor: Logger.log("i", "Ignoring auto-scaling, because %.3d < %.3d" % (1 / scale_factor, ignore_factor)) scale_factor = 1 pass elif self._application.getInstance().getPreferences().getValue("mesh/scale_tiny_meshes") == True and (scale_factor_width > 100 and scale_factor_height > 100 and scale_factor_depth > 100): # Round scale factor to lower factor of 10 to scale tiny object up (eg convert m to mm units) try: scale_factor = math.pow(10, math.floor(math.log(scale_factor) / math.log(10))) except: # In certain cases the scale_factor can be inf which can make this fail. Just use 1 instead. scale_factor = 1 else: scale_factor = 1 if scale_factor != 1: scale_vector = Vector(scale_factor, scale_factor, scale_factor) display_scale_factor = scale_factor * 100 scale_message = Message(i18n_catalog.i18nc("@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor)), title = i18n_catalog.i18nc("@info:title", "Scaling Object")) try: node.scale(scale_vector) scale_message.show() except Exception: Logger.logException("e", "While auto-scaling an exception has been raised")
def run(self): status_message = Message(i18n_catalog.i18nc( "@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0) status_message.show() scene = Application.getInstance().getController().getScene() node = scene.findObject(self._object_id) if not node and self._object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent( ).callDecoration("isGroup"): current_node = current_node.getParent() root = scene.getRoot() arranger = Arrange.create(scene_root=root) node_too_big = False if node.getBoundingBox().width < 300 or node.getBoundingBox( ).depth < 300: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode( current_node, min_offset=self._min_offset) else: node_too_big = True nodes = [] found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: node, solution_found = arranger.findNodePlacement( current_node, offset_shape_arr, hull_shape_arr) if node_too_big or not solution_found: found_solution_for_all = False new_location = node.getPosition() new_location = new_location.set(z=100 - i * 20) node.setPosition(new_location) nodes.append(node) Job.yieldThread() status_message.setProgress((i + 1) / self._count * 100) if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation( AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to find a location within the build volume for all objects" )) no_full_solution_message.show()
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent=None): super().__init__(device_id=device_id, address=address, properties=properties, parent=parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 self._authentication_id = None self._authentication_key = None self._authentication_counter = 0 self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) self._authentication_timer = QTimer() self._authentication_timer.setInterval( 1000) # TODO; Add preference for update interval self._authentication_timer.setSingleShot(False) self._authentication_timer.timeout.connect(self._onAuthenticationTimer) # The messages are created when connect is called the first time. # This ensures that the messages are only created for devices that actually want to connect. self._authentication_requested_message = None self._authentication_failed_message = None self._authentication_succeeded_message = None self._not_authenticated_message = None self.authenticationStateChanged.connect( self._onAuthenticationStateChanged) self.setPriority( 3 ) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription( i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self._output_controller = LegacyUM3PrinterOutputController(self) def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. self._setAcceptsCommands( self._authentication_state == AuthState.Authenticated) if self._authentication_state == AuthState.Authenticated: self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected over the network.")) elif self._authentication_state == AuthState.AuthenticationRequested: self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected over the network. Please approve the access request on the printer." )) elif self._authentication_state == AuthState.AuthenticationDenied: self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected over the network. No access to control the printer." )) def _setupMessages(self): self._authentication_requested_message = Message( i18n_catalog.i18nc( "@info:status", "Access to the printer requested. Please approve the request on the printer" ), lifetime=0, dismissable=False, progress=0, title=i18n_catalog.i18nc("@info:title", "Authentication status")) self._authentication_failed_message = Message( i18n_catalog.i18nc("@info:status", ""), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._authentication_failed_message.addAction( "Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect( self._messageCallback) self._authentication_succeeded_message = Message( i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message = Message(i18n_catalog.i18nc( "@info:status", "No access to print with this printer. Unable to send print job."), title=i18n_catalog.i18nc( "@info:title", "Authentication Status")) self._not_authenticated_message.addAction( "Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) self._not_authenticated_message.actionTriggered.connect( self._messageCallback) def _messageCallback(self, message_id=None, action_id="Retry"): if action_id == "Request" or action_id == "Retry": if self._authentication_failed_message: self._authentication_failed_message.hide() if self._not_authenticated_message: self._not_authenticated_message.hide() self._requestAuthentication() def connect(self): super().connect() self._setupMessages() global_container = Application.getInstance().getGlobalContainerStack() if global_container: self._authentication_id = global_container.getMetaDataEntry( "network_authentication_id", None) self._authentication_key = global_container.getMetaDataEntry( "network_authentication_key", None) def close(self): super().close() if self._authentication_requested_message: self._authentication_requested_message.hide() if self._authentication_failed_message: self._authentication_failed_message.hide() if self._authentication_succeeded_message: self._authentication_succeeded_message.hide() self._sending_gcode = False self._compressing_gcode = False self._authentication_timer.stop() ## Send all material profiles to the printer. def _sendMaterialProfiles(self): Logger.log("i", "Sending material profiles to printer") # TODO: Might want to move this to a job... for container in ContainerRegistry.getInstance( ).findInstanceContainers(type="material"): try: xml_data = container.serialize() if xml_data == "" or xml_data is None: continue names = ContainerManager.getInstance().getLinkedMaterials( container.getId()) if names: # There are other materials that share this GUID. if not container.isReadOnly(): continue # If it's not readonly, it's created by user, so skip it. file_name = "none.xml" self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None) except NotImplementedError: # If the material container is not the most "generic" one it can't be serialized an will raise a # NotImplementedError. We can simply ignore these. pass def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): if not self.activePrinter: # No active printer. Unable to write return if self.activePrinter.state not in ["idle", ""]: # Printer is not able to accept commands. return if self._authentication_state != AuthState.Authenticated: # Not authenticated, so unable to send job. return self.writeStarted.emit(self) gcode_dict = getattr( Application.getInstance().getController().getScene(), "gcode_dict", []) active_build_plate_id = Application.getInstance().getBuildPlateModel( ).activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: # Unable to find g-code. Nothing to send return self._gcode = gcode_list errors = self._checkForErrors() if errors: text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") informative_text = i18n_catalog.i18nc( "@label", "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. " "Please resolve this issues before continuing.") detailed_text = "" for error in errors: detailed_text += error + "\n" Application.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Ok, icon=QMessageBox.Critical, callback=self._messageBoxCallback) return # Don't continue; Errors must block sending the job to the printer. # There might be multiple things wrong with the configuration. Check these before starting. warnings = self._checkForWarnings() if warnings: text = i18n_catalog.i18nc( "@label", "Are you sure you wish to print with the selected configuration?" ) informative_text = i18n_catalog.i18nc( "@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " "For the best result, always slice for the PrintCores and materials that are inserted in your printer." ) detailed_text = "" for warning in warnings: detailed_text += warning + "\n" Application.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=self._messageBoxCallback) return # No warnings or errors, so we're good to go. self._startPrint() # Notify the UI that a switch to the print monitor should happen Application.getInstance().getController().setActiveStage( "MonitorStage") def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job." )) self._error_message.show() return self._sending_gcode = True self._send_gcode_start = time() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction( "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return file_name = "%s.gcode.gz" % Application.getInstance( ).getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, onFinished=self._onPostPrintJobFinished) return def _progressMessageActionTriggered(self, message_id=None, action_id=None): if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False Application.getInstance().getController().setActiveStage( "PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show( ) # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self._startPrint() else: Application.getInstance().getController().setActiveStage( "PrepareStage") # For some unknown reason Cura on OSX will hang if we do the call back code # immediately without first returning and leaving QML's event system. QTimer.singleShot(100, delayedCallback) def _checkForErrors(self): errors = [] print_information = Application.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log( "w", "There is no material length information. Unable to check for errors." ) return errors for index, extruder in enumerate(self.activePrinter.extruders): # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not. if extruder.hotendID == "": # No Printcore loaded. errors.append( i18n_catalog.i18nc( "@info:status", "No Printcore loaded in slot {slot_number}".format( slot_number=index + 1))) if index < len(print_information.materialLengths ) and print_information.materialLengths[index] != 0: # The extruder is by this print. if extruder.activeMaterial is None: # No active material errors.append( i18n_catalog.i18nc( "@info:status", "No material loaded in slot {slot_number}".format( slot_number=index + 1))) return errors def _checkForWarnings(self): warnings = [] print_information = Application.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log( "w", "There is no material length information. Unable to check for warnings." ) return warnings extruder_manager = ExtruderManager.getInstance() for index, extruder in enumerate(self.activePrinter.extruders): if index < len(print_information.materialLengths ) and print_information.materialLengths[index] != 0: # The extruder is by this print. # TODO: material length check # Check if the right Printcore is active. variant = extruder_manager.getExtruderStack( index).findContainer({"type": "variant"}) if variant: if variant.getName() != extruder.hotendID: warnings.append( i18n_catalog.i18nc( "@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}" .format( cura_printcore_name=variant.getName(), remote_printcore_name=extruder.hotendID, extruder_id=index + 1))) else: Logger.log("w", "Unable to find variant.") # Check if the right material is loaded. local_material = extruder_manager.getExtruderStack( index).findContainer({"type": "material"}) if local_material: if extruder.activeMaterial.guid != local_material.getMetaDataEntry( "GUID"): Logger.log( "w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID")) warnings.append( i18n_catalog.i18nc( "@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}" ).format(local_material.getName(), extruder.activeMaterial.name, index + 1)) else: Logger.log("w", "Unable to find material.") return warnings def _update(self): if not super()._update(): return if self._authentication_state == AuthState.NotAuthenticated: if self._authentication_id is None and self._authentication_key is None: # This machine doesn't have any authentication, so request it. self._requestAuthentication() elif self._authentication_id is not None and self._authentication_key is not None: # We have authentication info, but we haven't checked it out yet. Do so now. self._verifyAuthentication() elif self._authentication_state == AuthState.AuthenticationReceived: # We have an authentication, but it's not confirmed yet. self._checkAuthentication() # We don't need authentication for requesting info, so we can go right ahead with requesting this. self.get("printer", onFinished=self._onGetPrinterDataFinished) self.get("print_job", onFinished=self._onGetPrintJobFinished) def _resetAuthenticationRequestedMessage(self): if self._authentication_requested_message: self._authentication_requested_message.hide() self._authentication_timer.stop() self._authentication_counter = 0 def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress( self._authentication_counter / self._max_authentication_counter * 100) if self._authentication_counter > self._max_authentication_counter: self._authentication_timer.stop() Logger.log( "i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id) self.setAuthenticationState(AuthState.AuthenticationDenied) self._resetAuthenticationRequestedMessage() self._authentication_failed_message.show() def _verifyAuthentication(self): Logger.log("d", "Attempting to verify authentication") # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted) def _onVerifyAuthenticationCompleted(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 401: # Something went wrong; We somehow tried to verify authentication without having one. Logger.log("d", "Attempted to verify auth without having one.") self._authentication_id = None self._authentication_key = None self.setAuthenticationState(AuthState.NotAuthenticated) elif status_code == 403 and self._authentication_state != AuthState.Authenticated: # If we were already authenticated, we probably got an older message back all of the sudden. Drop that. Logger.log( "d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) # Now we know for sure that we are authenticated, send the material profiles to the machine. self._sendMaterialProfiles() def _checkAuthentication(self): Logger.log( "d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished) def _onCheckAuthenticationFinished(self, reply): if str(self._authentication_id) not in reply.url().toString(): Logger.log("w", "Got an old id response.") # Got response for old authentication ID. return try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid authentication check from printer: Not valid JSON." ) return if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") self.setAuthenticationState(AuthState.Authenticated) self._saveAuthentication() # Double check that everything went well. self._verifyAuthentication() # Notify the user. self._resetAuthenticationRequestedMessage() self._authentication_succeeded_message.show() elif data.get("message", "") == "unauthorized": Logger.log("i", "Authentication was denied.") self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() def _saveAuthentication(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack: if "network_authentication_key" in global_container_stack.getMetaData( ): global_container_stack.setMetaDataEntry( "network_authentication_key", self._authentication_key) else: global_container_stack.addMetaDataEntry( "network_authentication_key", self._authentication_key) if "network_authentication_id" in global_container_stack.getMetaData( ): global_container_stack.setMetaDataEntry( "network_authentication_id", self._authentication_id) else: global_container_stack.addMetaDataEntry( "network_authentication_id", self._authentication_id) # Force save so we are sure the data is not lost. Application.getInstance().saveStack(global_container_stack) Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) else: Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) def _onRequestAuthenticationFinished(self, reply): try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid authentication request reply from printer: Not valid JSON." ) self.setAuthenticationState(AuthState.NotAuthenticated) return self.setAuthenticationState(AuthState.AuthenticationReceived) self._authentication_id = data["id"] self._authentication_key = data["key"] Logger.log( "i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) def _requestAuthentication(self): self._authentication_requested_message.show() self._authentication_timer.start() # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might # give issues. self._authentication_key = None self._authentication_id = None self.post("auth/request", json.dumps({ "application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName() }).encode(), onFinished=self._onRequestAuthenticationFinished) self.setAuthenticationState(AuthState.AuthenticationRequested) def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: Logger.log( "d", "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", self._id, self._authentication_id, self._getSafeAuthKey()) authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) else: Logger.log( "d", "No authentication is available to use for %s, but we did got a request for it.", self._id) def _onGetPrintJobFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid print job state message: Not valid JSON." ) return if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job.updateState(result["state"]) print_job.updateTimeElapsed(result["time_elapsed"]) print_job.updateTimeTotal(result["time_total"]) print_job.updateName(result["name"]) elif status_code == 404: # No job found, so delete the active print job (if any!) printer.updateActivePrintJob(None) else: Logger.log( "w", "Got status code {status_code} while trying to get printer data" .format(status_code=status_code)) def materialHotendChangedMessage(self, callback): Application.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Sync with your printer"), i18n_catalog.i18nc( "@label", "Would you like to use your current printer configuration in Cura?" ), i18n_catalog.i18nc( "@label", "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer." ), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback) def _onGetPrinterDataFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid printer state message: Not valid JSON." ) return if not self._printers: # Quickest way to get the firmware version is to grab it from the zeroconf. firmware_version = self._properties.get( b"firmware_version", b"").decode("utf-8") self._printers = [ PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version) ] self._printers[0].setCamera( NetworkCamera("http://" + self._address + ":8080/?action=stream")) for extruder in self._printers[0].extruders: extruder.activeMaterialChanged.connect( self.materialIdChanged) extruder.hotendIDChanged.connect(self.hotendIdChanged) self.printersChanged.emit() # LegacyUM3 always has a single printer. printer = self._printers[0] printer.updateBedTemperature( result["bed"]["temperature"]["current"]) printer.updateTargetBedTemperature( result["bed"]["temperature"]["target"]) printer.updateState(result["status"]) try: # If we're still handling the request, we should ignore remote for a bit. if not printer.getController().isPreheatRequestInProgress(): printer.updateIsPreheating( result["bed"]["pre_heat"]["active"]) except KeyError: # Older firmwares don't support preheating, so we need to fake it. pass head_position = result["heads"][0]["position"] printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) for index in range(0, self._number_of_extruders): temperatures = result["heads"][0]["extruders"][index][ "hotend"]["temperature"] extruder = printer.extruders[index] extruder.updateTargetHotendTemperature(temperatures["target"]) extruder.updateHotendTemperature(temperatures["current"]) material_guid = result["heads"][0]["extruders"][index][ "active_material"]["guid"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid: # Find matching material (as we need to set brand, type & color) containers = ContainerRegistry.getInstance( ).findInstanceContainers(type="material", GUID=material_guid) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry( "material") name = containers[0].getName() else: # Unknown material. color = "#00000000" brand = "Unknown" material_type = "Unknown" name = "Unknown" material = MaterialOutputModel(guid=material_guid, type=material_type, brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) try: hotend_id = result["heads"][0]["extruders"][index][ "hotend"]["id"] except KeyError: hotend_id = "" printer.extruders[index].updateHotendID(hotend_id) else: Logger.log( "w", "Got status code {status_code} while trying to get printer data" .format(status_code=status_code)) ## Convenience function to "blur" out all but the last 5 characters of the auth key. # This can be used to debug print the key, without it compromising the security. def _getSafeAuthKey(self): if self._authentication_key is not None: result = self._authentication_key[-5:] result = "********" + result return result return self._authentication_key
class RotateTool(Tool): def __init__(self): super().__init__() self._handle = RotateToolHandle.RotateToolHandle() self._snap_rotation = True self._snap_angle = math.radians(15) self._angle = None self._angle_update_time = None self._progress_message = None self._iterations = 0 self._total_iterations = 0 self._rotating = False self.setExposedProperties("ToolHint", "RotationSnap", "RotationSnapAngle") self._saved_node_positions = [] ## Handle mouse and keyboard events # # \param event type(Event) def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: # Snap is toggled when pressing the shift button self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): # Start a rotate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if self._handle.isAxis(id): self.setLockedAxis(id) else: # Not clicked on an axis: do nothing. return False handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in Selection.getAllSelectedObjects(): self._saved_node_positions.append((node, node.getPosition())) if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) else: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) self.setDragStart(event.x, event.y) self._rotating = False self._angle = 0 if event.type == Event.MouseMoveEvent: # Perform a rotate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) if not self._rotating: self._rotating = True self.operationStarted.emit(self) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalized() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return drag_end = (drag_position - handle_position).normalized() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) else: direction = -1 # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time self._angle += direction * angle self.propertyChanged.emit() # Rotate around the saved centeres of all selected nodes op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation(RotateOperation(node, rotation, rotate_around_point = position)) op.push() self.setDragStart(event.x, event.y) if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._angle = None self.propertyChanged.emit() if self._rotating: self.operationStopped.emit(self) return True ## Return a formatted angle of the current rotate operation # # \return type(String) fully formatted string showing the angle by which the mesh(es) are rotated def getToolHint(self): return "%d°" % round(math.degrees(self._angle)) if self._angle else None ## Get the state of the "snap rotation to N-degree increments" option # # \return type(Boolean) def getRotationSnap(self): return self._snap_rotation ## Set the state of the "snap rotation to N-degree increments" option # # \param snap type(Boolean) def setRotationSnap(self, snap): if snap != self._snap_rotation: self._snap_rotation = snap self.propertyChanged.emit() ## Get the number of degrees used in the "snap rotation to N-degree increments" option # # \return type(Number) def getRotationSnapAngle(self): return self._snap_angle ## Set the number of degrees used in the "snap rotation to N-degree increments" option # # \param snap type(Number) def setRotationSnapAngle(self, angle): if angle != self._snap_angle: self._snap_angle = angle self.propertyChanged.emit() ## Reset the orientation of the mesh(es) to their original orientation(s) def resetRotation(self): Selection.applyOperation(SetTransformOperation, None, Quaternion(), None) ## Initialise and start a LayFlatOperation # # Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project def layFlat(self): self.operationStarted.emit(self) self._progress_message = Message("Laying object flat on buildplate...", lifetime = 0, dismissable = False) self._progress_message.setProgress(0) self._iterations = 0 self._total_iterations = 0 for selected_object in Selection.getAllSelectedObjects(): if not selected_object.callDecoration("isGroup"): self._total_iterations += len(selected_object.getMeshDataTransformed().getVertices()) * 2 else: for child in selected_object.getChildren(): self._total_iterations += len(child.getMeshDataTransformed().getVertices()) * 2 self._progress_message.show() operations = Selection.applyOperation(LayFlatOperation) for op in operations: op.progress.connect(self._layFlatProgress) job = LayFlatJob(operations) job.finished.connect(self._layFlatFinished) job.start() ## Called while performing the LayFlatOperation so progress can be shown # # Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications, # \param iterations type(int) number of iterations performed since the last callback def _layFlatProgress(self, iterations): self._iterations += iterations self._progress_message.setProgress(100 * self._iterations / self._total_iterations) ## Called when the LayFlatJob is done running all of its LayFlatOperations # # \param job type(LayFlatJob) def _layFlatFinished(self, job): if self._progress_message: self._progress_message.hide() self._progress_message = None self.operationStopped.emit(self)
def requestWrite(self, node, file_name=None, filter_by_machine=False): if self._writing: raise OutputDeviceError.DeviceBusyError() self._writing = True # load settings if Preferences.getInstance().getValue("MPSelectMini/ip"): ip = Preferences.getInstance().getValue("MPSelectMini/ip") if Preferences.getInstance().getValue("MPSelectMini/start_print"): start_print = Preferences.getInstance().getValue( "MPSelectMini/start_print") try: start_print except NameError: start_print = False # check for valid ip if not ip: raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc("@info:status", "Invalid IP")) # Get GCode file_formats = Application.getInstance().getMeshFileHandler( ).getSupportedFileTypesWrite() mesh_writer = Application.getInstance().getMeshFileHandler( ).getWriterByMimeType("text/x-gcode") stream = io.StringIO() mesh_writer.write(stream, node, MeshWriter.OutputMode.TextMode) gcode = stream.getvalue() try: # Upload GCode s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, 80)) s.send(bytes('POST /upload HTTP/1.1\r\n', 'ascii')) s.send( bytes( 'Content-Type: multipart/form-data; boundary=------------------------2d30fc993bb09c6a\r\n', 'ascii')) s.send(bytes('\r\n', 'ascii')) s.send( bytes('--------------------------2d30fc993bb09c6a\r\n', 'ascii')) s.send( bytes( 'Content-Disposition: form-data; name="filedata"; filename="cache.gc"\r\n', 'ascii')) s.send(bytes('Content-Type: application/octet-stream\r\n', 'ascii')) s.send(bytes('\r\n', 'ascii')) s.send(bytes(gcode, 'ascii')) s.send(bytes('\r\n', 'ascii')) s.send( bytes('--------------------------2d30fc993bb09c6a--\r\n', 'ascii')) response = str(s.recv(1024), 'ascii') s.close() # Check upload response if response.startswith('HTTP/1.1 200 OK'): message = Message( catalog.i18nc("@info:status", "Upload Success")) message.show() else: Logger.log( "e", "Invalid http response uploading gcode:\n" + response) raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc("@info:status", "Upload Failed")) # Send cancel print (prevents upload bug where printer will start before heating extruder) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, 80)) s.send(bytes('GET /set?cmd={P:X} HTTP/1.1\r\n', 'ascii')) s.send(bytes('\r\n', 'ascii')) response = str(s.recv(1024), 'ascii') s.close() if start_print: # Send start print s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, 80)) s.send(bytes('GET /set?code=M565 HTTP/1.1\r\n', 'ascii')) s.send(bytes('\r\n', 'ascii')) response = str(s.recv(1024), 'ascii') s.close() # Check start print response if response.startswith('HTTP/1.1 200 OK'): message = Message( catalog.i18nc("@info:status", "<b>Printing Started</b>")) message.show() else: Logger.log( "e", "Invalid http response starting print job:\n" + response) raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc("@info:status", "Start Print Failed")) except (TimeoutError): raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc("@info:status", "Connection Timeout")) self._writing = False
def _installPackage(self, installation_package_data: Dict[str, Any]) -> None: package_info = installation_package_data["package_info"] filename = installation_package_data["filename"] package_id = package_info["package_id"] Logger.log("i", "Installing package [%s] from file [%s]", package_id, filename) # Load the cached package file and extract all contents to a temporary directory if not os.path.exists(filename): Logger.log( "w", "Package [%s] file '%s' is missing, cannot install this package", package_id, filename) return try: with zipfile.ZipFile(filename, "r") as archive: name_list = archive.namelist() temp_dir = tempfile.TemporaryDirectory() for archive_filename in name_list: archive.extract(archive_filename, temp_dir.name) QCoreApplication.processEvents() except Exception: Logger.logException("e", "Failed to install package from file [%s]", filename) return # Remove it first and then install try: self._purgePackage(package_id) except Exception as e: message = Message(catalog.i18nc( "@error:update", "There was an error uninstalling the package {package} before installing " "new version:\n{error}.\nPlease try to upgrade again later.". format(package=package_id, error=str(e))), title=catalog.i18nc("@info:title", "Updating error"), message_type=Message.MessageType.ERROR) message.show() return # Copy the folders there for sub_dir_name, installation_root_dir in self._installation_dirs_dict.items( ): src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name) dst_dir_path = os.path.join(installation_root_dir, package_id) if not os.path.exists(src_dir_path): Logger.log( "w", "The path %s does not exist, so not installing the files", src_dir_path) continue try: self.__installPackageFiles(package_id, src_dir_path, dst_dir_path) except EnvironmentError as e: Logger.log( "e", "Can't install package due to EnvironmentError: {err}". format(err=str(e))) continue # Remove the file try: os.remove(filename) except Exception: Logger.log("w", "Tried to delete file [%s], but it failed", filename) # Move the info to the installed list of packages only when it succeeds self._installed_package_dict[ package_id] = self._to_install_package_dict[package_id] self._installed_package_dict[package_id]["package_info"][ "is_installed"] = True
class QidiPrintOutputDevice(PrinterOutputDevice): printerStatusChanged = pyqtSignal() def __init__(self, name, address): super().__init__(name, connection_type=ConnectionType.NetworkConnection) self.setShortDescription( catalog.i18nc("@action:button Preceded by 'Ready to'.", "Send to " + name)) self.setDescription(catalog.i18nc("@info:tooltip", "Send to " + name)) self.setConnectionText( catalog.i18nc("@info:status", "Connected via Network")) self.setName(name) self.setIconName("print") self._properties = {} self._address = address self._PluginName = 'QIDI Print' self.setPriority(3) self._application = CuraApplication.getInstance() self._preferences = Application.getInstance().getPreferences() self._preferences.addPreference("QidiPrint/autoprint", False) self._autoPrint = self._preferences.getValue("QidiPrint/autoprint") self._update_timer.setInterval(1000) self._output_controller = GenericOutputController(self) self._output_controller.setCanUpdateFirmware(False) # Set when print is started in order to check running time. self._print_start_time = None # type: Optional[float] self._print_estimated_time = None # type: Optional[int] self._accepts_commands = True # from PrinterOutputDevice self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'qml', 'MonitorItem.qml') self._localTempGcode = Resources.getStoragePath( Resources.Resources, 'data.gcode') self._qidi = QidiConnectionManager(self._address, self._localTempGcode, False) self._qidi.progressChanged.connect(self._update_progress) self._qidi.conectionStateChanged.connect(self._conectionStateChanged) self._qidi.updateDone.connect(self._update_status) self._stage = OutputStage.ready Logger.log("d", self._name + " | New QidiPrintOutputDevice created") Logger.log("d", self._name + " | IP: " + self._address) if hasattr(self, '_message'): self._message.hide() self._message = None def _update_progress(self, progress): if self._message: self._message.setProgress(int(progress)) self.writeProgress.emit(self, progress) def _conectionStateChanged(self, new_state): if new_state == True: container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() num_extruders = container_stack.getProperty( "machine_extruder_count", "value") # Ensure that a printer is created. printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=num_extruders, firmware_version=self.firmwareVersion) printer.updateName(container_stack.getName()) self._printers = [printer] self.setConnectionState(ConnectionState.Connected) self.printersChanged.emit() else: #self._printers = None self.setConnectionState(ConnectionState.Connecting) if self.printers[0]: self.printers[0].updateState("offline") def _update(self): if self._qidi._connected == False: Thread(target=self._qidi.connect, daemon=True, name="Qidi Connect").start() self.printerStatusChanged.emit() return if self.connectionState != ConnectionState.Connected: self.setConnectionState(ConnectionState.Connected) Thread(target=self._qidi.update, daemon=True, name="Qidi Update").start() def close(self): super().close() if self._message: self._message.hide() self.printerStatusChanged.emit() def pausePrint(self): self.sendCommand("M25") def resumePrint(self): self.sendCommand("M24") def cancelPrint(self): self._cancelPrint = True self.sendCommand("M33") def _update_status(self): printer = self.printers[0] status = self._qidi._status if "bed_nowtemp" in status: printer.updateBedTemperature(int(status["bed_nowtemp"])) if "bed_targettemp" in status: printer.updateTargetBedTemperature(int(status["bed_targettemp"])) extruder = printer.extruders[0] if "e1_nowtemp" in status: extruder.updateHotendTemperature(int(status["e1_nowtemp"])) if "e1_targettemp" in status: extruder.updateTargetHotendTemperature(int( status["e1_targettemp"])) if len(printer.extruders) > 1: extruder = printer.extruders[1] if "e2_nowtemp" in status: extruder.updateHotendTemperature(int(status["e2_nowtemp"])) if "e2_targettemp" in status: extruder.updateTargetHotendTemperature( int(status["e2_targettemp"])) if self._qidi._isPrinting: if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob elapsed = self._qidi._printing_time print_job.updateTimeElapsed(int(self._qidi._printing_time)) print_job.updateName(self._qidi._printing_filename) if self._qidi._print_total > 0: progress = float(self._qidi._print_now) / float( self._qidi._print_total) if progress > 0: print_job.updateTimeTotal( int(self._qidi._printing_time / progress)) if self._qidi._isIdle: if self._cancelPrint: job_state = 'aborting' else: job_state = 'paused' else: job_state = 'printing' print_job.updateState(job_state) else: if printer.activePrintJob: printer.updateActivePrintJob(None) job_state = 'idle' self._cancelPrint = False print_job = None printer.updateState(job_state) self.printerStatusChanged.emit() def requestWrite(self, node, fileName=None, *args, **kwargs): if self._stage != OutputStage.ready or self._qidi._isPrinting: Message(catalog.i18nc('@info:status', 'Cannot Print, printer is busy'), title=catalog.i18nc("@info:title", "BUSY")).show() raise OutputDeviceError.DeviceBusyError() # Make sure post-processing plugin are run on the gcode self.writeStarted.emit(self) if fileName: fileName = os.path.splitext(fileName)[0] else: fileName = "%s" % Application.getInstance().getPrintInformation( ).jobName self.targetSendFileName = fileName path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'qml', 'UploadFilename.qml') self._dialog = CuraApplication.getInstance().createQmlComponent( path, {"manager": self}) self._dialog.textChanged.connect(self.onFilenameChanged) self._dialog.accepted.connect(self.onFilenameAccepted) self._dialog.show() self._dialog.findChild(QObject, "autoPrint").setProperty( 'checked', self._autoPrint) self._dialog.findChild(QObject, "nameField").setProperty( 'text', self.targetSendFileName) self._dialog.findChild(QObject, "nameField").select( 0, len(self.targetSendFileName)) self._dialog.findChild(QObject, "nameField").setProperty('focus', True) def onFilenameChanged(self): fileName = self._dialog.findChild( QObject, "nameField").property('text').strip() forbidden_characters = "\"'´`<>()[]?*\,;:&%#$!" for forbidden_character in forbidden_characters: if forbidden_character in fileName: self._dialog.setProperty('validName', False) self._dialog.setProperty( 'validationError', 'Filename cannot contain {}'.format(forbidden_characters)) return if fileName == '.' or fileName == '..': self._dialog.setProperty('validName', False) self._dialog.setProperty('validationError', 'Filename cannot be "." or ".."') return self._dialog.setProperty('validName', len(fileName) > 0) self._dialog.setProperty('validationError', 'Filename too short') def startSendingThread(self): Logger.log('i', '=============QIDI SEND BEGIN============') self._errorMsg = '' self._qidi._abort = False self._stage = OutputStage.writing res = self._qidi.sendfile(self.targetSendFileName) if self._message: self._message.hide() self._message = None # type:Optional[Message] self.writeFinished.emit(self) self._stage = OutputStage.ready if res == QidiResult.SUCCES: if self._autoPrint is False: self._message = Message( catalog.i18nc("@info:status", "Do you wish to print now?"), title=catalog.i18nc("@label", "SUCCESS")) self._message.addAction("PRINT", catalog.i18nc("@action:button", "YES"), None, "") self._message.addAction("NO", catalog.i18nc("@action:button", "NO"), None, "") self._message.actionTriggered.connect(self._onActionTriggered) self._message.setProgress(None) self._message.show() else: self._onActionTriggered(self._message, "PRINT") self.writeSuccess.emit(self) self._stage = OutputStage.ready return self.writeError.emit(self) if res == QidiResult.ABORTED: Message(catalog.i18nc('@info:status', 'Upload Canceled'), title=catalog.i18nc("@info:title", "ABORTED")).show() return result_msg = "Unknown Error!!!" if self._result == QidiResult.TIMEOUT: result_msg = 'Connection timeout' elif self._result == QidiResult.WRITE_ERROR: self.writeError.emit(self) result_msg = self._errorMsg if 'create file' in self._errorMsg: m = Message(catalog.i18nc( '@info:status', ' Write error, please check that the SD card /U disk has been inserted' ), lifetime=0) m.show() elif self._result == QidiResult.FILE_EMPTY: self.writeError.emit(self) result_msg = 'File empty' elif self._result == QidiResult.FILE_NOT_OPEN: self.writeError.emit(self) result_msg = "Cannot Open File" self._message = Message(catalog.i18nc("@info:status", result_msg), title=catalog.i18nc("@label", "FAILURE")) self._message.show() Logger.log('e', result_msg) def updateChamberFan(self): global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return cooling_chamber = global_container_stack.getProperty( "cooling_chamber", "value") if cooling_chamber == False: return cooling_chamber_at_layer = global_container_stack.getProperty( "cooling_chamber_at_layer", "value") scene = self._application.getController().getScene() gcode_dict = getattr(scene, "gcode_dict", {}) if not gcode_dict: return data = gcode_dict[0] for layer in data: lines = layer.split("\n") for line in lines: if ";LAYER:" in line: index = data.index(layer) current_layer = int(line.split(":")[1]) if current_layer == cooling_chamber_at_layer: layer = "M106 T-2 ;Enable chamber loop\n" + layer data[index] = layer data[ -1] = "M107 T-2 ;Disable chamber loop\n" + data[-1] setattr(scene, "gcode_dict", gcode_dict) return def onFilenameAccepted(self): self.targetSendFileName = self._dialog.findChild( QObject, "nameField").property('text').strip() autoprint = self._dialog.findChild(QObject, "autoPrint").property('checked') if autoprint != self._autoPrint: self._autoPrint = autoprint self._preferences.setValue("QidiPrint/autoprint", self._autoPrint) Logger.log( "d", self._name + " | Filename set to: " + self.targetSendFileName) self._dialog.deleteLater() self.updateChamberFan() success = False with open(self._localTempGcode, 'w+', buffering=1) as fp: if fp: writer = ChituCodeWriter() success = writer.write(fp, None, MeshWriter.OutputMode.TextMode) if success: self._message = Message( catalog.i18nc("@info:status", "Uploading to {}").format(self._name), title=catalog.i18nc("@label", "Print jobs"), progress=-1, lifetime=0, dismissable=False, use_inactivity_timer=False) self._message.addAction("ABORT", catalog.i18nc("@action:button", "Cancel"), None, "") self._message.actionTriggered.connect(self._onActionTriggered) self._message.show() Thread(target=self.startSendingThread, daemon=True, name=self._name + " File Send").start() else: self._message = Message(catalog.i18nc("@info:status", "Cannot create gcode file!"), title=catalog.i18nc("@label", "FAILURE")) self._message.show() def _onActionTriggered(self, message, action): if self._message: self._message.hide() self._message = None # type:Optional[Message] if action == "PRINT": res = self._qidi.print() if res is not QidiResult.SUCCES: Message(catalog.i18nc('@info:status', 'Cannot Print'), title=catalog.i18nc("@info:title", "FAILURE")).show() else: CuraApplication.getInstance().getController().setActiveStage( "MonitorStage") elif action == "ABORT": Logger.log("i", "Stopping upload because the user pressed cancel.") self._qidi._abort = True def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" @pyqtSlot(str) def sendCommand(self, cmd): if isinstance(cmd, str): self._qidi.sendCommand(cmd) elif isinstance(cmd, list): for eachCommand in cmd: self._qidi.sendCommand(eachCommand) @pyqtProperty(str, notify=printerStatusChanged) def status(self): return str(self._connection_state).split('.')[1] @pyqtProperty(str, constant=True) def name(self): return self._name @pyqtProperty(str, notify=printerStatusChanged) def firmwareVersion(self): return self.getFirmwareName() def getFirmwareName(self): return self._qidi._firmware_ver @pyqtProperty(str, notify=printerStatusChanged) def xPosition(self) -> bool: if "x_pos" in self._qidi._status: return self._qidi._status["x_pos"][:-1] else: return "" @pyqtProperty(str, notify=printerStatusChanged) def yPosition(self) -> bool: if "y_pos" in self._qidi._status: return self._qidi._status["y_pos"][:-1] else: return "" @pyqtProperty(str, notify=printerStatusChanged) def zPosition(self) -> bool: if "z_pos" in self._qidi._status: return self._qidi._status["z_pos"][:-1] else: return "" @pyqtProperty(str, notify=printerStatusChanged) def coolingFan(self) -> bool: if "fan" in self._qidi._status: fan = float(self._qidi._status["fan"]) return "{}".format(int(fan / 2.55)) else: return ""
def __init__(self): super().__init__() self._max_layers = 0 self._current_layer_num = 0 self._minimum_layer_num = 0 self._current_layer_mesh = None self._current_layer_jumps = None self._top_layers_job = None self._activity = False self._old_max_layers = 0 self._max_paths = 0 self._current_path_num = 0 self._minimum_path_num = 0 self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged) self._busy = False self._simulation_running = False self._ghost_shader = None self._layer_pass = None self._composite_pass = None self._old_layer_bindings = None self._simulationview_composite_shader = None self._old_composite_shader = None self._global_container_stack = None self._proxy = SimulationViewProxy() self._controller.getScene().getRoot().childrenChanged.connect( self._onSceneChanged) self._resetSettings() self._legend_items = None self._show_travel_moves = False self._nozzle_node = None Preferences.getInstance().addPreference("view/top_layer_count", 5) Preferences.getInstance().addPreference("view/only_show_top_layers", False) Preferences.getInstance().addPreference( "view/force_layer_view_compatibility_mode", False) Preferences.getInstance().addPreference("layerview/layer_view_type", 0) Preferences.getInstance().addPreference("layerview/extruder_opacities", "") Preferences.getInstance().addPreference("layerview/show_travel_moves", False) Preferences.getInstance().addPreference("layerview/show_helpers", True) Preferences.getInstance().addPreference("layerview/show_skin", True) Preferences.getInstance().addPreference("layerview/show_infill", True) Preferences.getInstance().preferenceChanged.connect( self._onPreferencesChanged) self._updateWithPreferences() self._solid_layers = int( Preferences.getInstance().getValue("view/top_layer_count")) self._only_show_top_layers = bool( Preferences.getInstance().getValue("view/only_show_top_layers")) self._compatibility_mode = True # for safety self._wireprint_warning_message = Message(catalog.i18nc( "@info:status", "Cura does not accurately display layers when Wire Printing is enabled" ), title=catalog.i18nc( "@info:title", "Simulation View"))
class Scene: """Container object for the scene graph The main purpose of this class is to provide the root SceneNode. """ def __init__(self) -> None: super().__init__() from UM.Scene.SceneNode import SceneNode self._root = SceneNode(name="Root") self._root.setCalculateBoundingBox(False) self._connectSignalsRoot() self._active_camera = None # type: Optional[Camera] self._ignore_scene_changes = False self._lock = threading.Lock() # Watching file for changes. self._file_watcher = QFileSystemWatcher() self._file_watcher.fileChanged.connect(self._onFileChanged) self._reload_message = None # type: Optional[Message] self._callbacks = set( ) # type: Set[Callable] # Need to keep these in memory. This is a memory leak every time you refresh, but a tiny one. def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: self._ignore_scene_changes = ignore_scene_changes if self._ignore_scene_changes: self._disconnectSignalsRoot() else: self._connectSignalsRoot() @deprecated("Scene lock is no longer used", "4.5") def getSceneLock(self) -> threading.Lock: return self._lock def getRoot(self) -> "SceneNode": """Get the root node of the scene.""" return self._root def setRoot(self, node: "SceneNode") -> None: """Change the root node of the scene""" if self._root != node: if not self._ignore_scene_changes: self._disconnectSignalsRoot() self._root = node if not self._ignore_scene_changes: self._connectSignalsRoot() self.rootChanged.emit() rootChanged = Signal() def getActiveCamera(self) -> Optional[Camera]: """Get the camera that should be used for rendering.""" return self._active_camera def getAllCameras(self) -> List[Camera]: cameras = [] for node in BreadthFirstIterator(self._root): if isinstance(node, Camera): cameras.append(node) return cameras def setActiveCamera(self, name: str) -> None: """Set the camera that should be used for rendering. :param name: The name of the camera to use. """ camera = self.findCamera(name) if camera and camera != self._active_camera: if self._active_camera: self._active_camera.perspectiveChanged.disconnect( self.sceneChanged) self._active_camera = camera self._active_camera.perspectiveChanged.connect(self.sceneChanged) else: Logger.log( "w", "Couldn't find camera with name [%s] to activate!" % name) sceneChanged = Signal() """Signal that is emitted whenever something in the scene changes. :param object: The object that triggered the change. """ def findObject(self, object_id: int) -> Optional["SceneNode"]: """Find an object by id. :param object_id: The id of the object to search for, as returned by the python id() method. :return: The object if found, or None if not. """ for node in BreadthFirstIterator(self._root): if id(node) == object_id: return node return None def findCamera(self, name: str) -> Optional[Camera]: for node in BreadthFirstIterator(self._root): if isinstance(node, Camera) and node.getName() == name: return node return None def addWatchedFile(self, file_path: str) -> None: """Add a file to be watched for changes. :param file_path: The path to the file that must be watched. """ # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.addPath(file_path) def removeWatchedFile(self, file_path: str) -> None: """Remove a file so that it will no longer be watched for changes. :param file_path: The path to the file that must no longer be watched. """ # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.removePath(file_path) def _onFileChanged(self, file_path: str) -> None: """Triggered whenever a file is changed that we currently have loaded.""" try: if os.path.getsize(file_path) == 0: # File is empty. return except EnvironmentError: # Or it doesn't exist any more, or we have no access any more. return # Multiple nodes may be loaded from the same file at different stages. Reload them all. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # To find which nodes to reload when files have changed. modified_nodes = [ node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path ] # type: ignore if modified_nodes: # Hide the message if it was already visible if self._reload_message is not None: self._reload_message.hide() self._reload_message = Message(i18n_catalog.i18nc( "@info", "Would you like to reload {filename}?").format( filename=os.path.basename(file_path)), title=i18n_catalog.i18nc( "@info:title", "File has been modified")) self._reload_message.addAction( "reload", i18n_catalog.i18nc("@action:button", "Reload"), icon="", description=i18n_catalog.i18nc( "@action:description", "This will trigger the modified files to reload from disk." )) self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: """Reloads a list of nodes after the user pressed the "Reload" button. :param nodes: The list of nodes that needs to be reloaded. :param message: The message that triggered the action to reload them. :param action: The button that triggered the action to reload them. """ if action != "reload": return if self._reload_message is not None: self._reload_message.hide() for node in nodes: meshdata = node.getMeshData() if meshdata: filename = meshdata.getFileName() if not filename or not os.path.isfile( filename): # File doesn't exist any more. continue job = ReadMeshJob(filename) reload_finished_callback = functools.partial( self._reloadJobFinished, node) self._callbacks.add( reload_finished_callback ) #Store it so it won't get garbage collected. This is a memory leak, but just one partial per reload so it's not much. job.finished.connect(reload_finished_callback) job.start() def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: """Triggered when reloading has finished. This then puts the resulting mesh data in the node. """ for node in job.getResult(): mesh_data = node.getMeshData() if mesh_data: replaced_node.setMeshData(mesh_data) else: Logger.log("w", "Could not find a mesh in reloaded node.")
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() global_container_stack = Application.getInstance().getGlobalContainerStack() machine_width = global_container_stack.getProperty("machine_width", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value") x, y = machine_width, machine_depth arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = []) arrange_array.add() # Place nodes one at a time start_priority = 0 grouped_operation = GroupedOperation() found_solution_for_all = True left_over_nodes = [] # nodes that do not fit on an empty build plate for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). try_placement = True current_build_plate_number = 0 # always start with the first one while try_placement: # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects while current_build_plate_number >= arrange_array.count(): arrange_array.add() arranger = arrange_array.get(current_build_plate_number) best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place arranger.place(x, y, offset_shape_arr) # place the object in the arranger node.callDecoration("setBuildPlateNumber", current_build_plate_number) grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) try_placement = False else: # very naive, because we skip to the next build plate if one model doesn't fit. if arranger.isEmpty: # apparently we can never place this object left_over_nodes.append(node) try_placement = False else: # try next build plate current_build_plate_number += 1 try_placement = True status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() for node in left_over_nodes: node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate found_solution_for_all = False grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show()
class BuildVolume(SceneNode): raftThicknessChanged = Signal() def __init__(self, parent = None): super().__init__(parent) self._volume_outline_color = None self._x_axis_color = None self._y_axis_color = None self._z_axis_color = None self._disallowed_area_color = None self._error_area_color = None self._width = 0 self._height = 0 self._depth = 0 self._shape = "" self._shader = None self._origin_mesh = None self._origin_line_length = 20 self._origin_line_width = 0.5 self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self._error_areas = [] self._error_mesh = None self.setCalculateBoundingBox(False) self._volume_aabb = None self._raft_thickness = 0.0 self._extra_z_clearance = 0.0 self._adhesion_type = None self._platform = Platform(self) self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged) self._onStackChanged() self._engine_ready = False Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) self._has_errors = False Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged) #Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() self._change_timer = QTimer() self._change_timer.setInterval(100) self._change_timer.setSingleShot(True) self._change_timer.timeout.connect(self._onChangeTimerFinished) self._build_volume_message = Message(catalog.i18nc("@info:status", "The build volume height has been reduced due to the value of the" " \"Print Sequence\" setting to prevent the gantry from colliding" " with printed models.")) # Must be after setting _build_volume_message, apparently that is used in getMachineManager. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # Therefore this works. Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged) # This should also ways work, and it is semantically more correct, # but it does not update the disallowed areas after material change Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged) def _onSceneChanged(self, source): if self._global_container_stack: self._change_timer.start() def _onChangeTimerFinished(self): root = Application.getInstance().getController().getScene().getRoot() new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode) if new_scene_objects != self._scene_objects: for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene. node.decoratorsChanged.connect(self._onNodeDecoratorChanged) for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene. per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal") if active_extruder_changed is not None: node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild) node.decoratorsChanged.disconnect(self._onNodeDecoratorChanged) self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. ## Updates the listeners that listen for changes in per-mesh stacks. # # \param node The node for which the decorators changed. def _onNodeDecoratorChanged(self, node): per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged) active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal") if active_extruder_changed is not None: active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild) self._updateDisallowedAreasAndRebuild() def setWidth(self, width): if width: self._width = width def setHeight(self, height): if height: self._height = height def setDepth(self, depth): if depth: self._depth = depth def setShape(self, shape): if shape: self._shape = shape def getDisallowedAreas(self): return self._disallowed_areas def setDisallowedAreas(self, areas): self._disallowed_areas = areas def render(self, renderer): if not self.getMeshData(): return True if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader")) self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader")) theme = Application.getInstance().getTheme() self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate").getRgb())) self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_alt").getRgb())) renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines) renderer.queueNode(self, mesh = self._origin_mesh) renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True) if self._disallowed_area_mesh: renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9) if self._error_mesh: renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True, backface_cull=True, sort=-8) return True ## Recalculates the build volume & disallowed areas. def rebuild(self): if not self._width or not self._height or not self._depth: return if not Application.getInstance()._engine: return if not self._volume_outline_color: theme = Application.getInstance().getTheme() self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb()) self._x_axis_color = Color(*theme.getColor("x_axis").getRgb()) self._y_axis_color = Color(*theme.getColor("y_axis").getRgb()) self._z_axis_color = Color(*theme.getColor("z_axis").getRgb()) self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb()) self._error_area_color = Color(*theme.getColor("error_area").getRgb()) min_w = -self._width / 2 max_w = self._width / 2 min_h = 0.0 max_h = self._height min_d = -self._depth / 2 max_d = self._depth / 2 z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting if self._shape != "elliptic": # Outline 'cube' of the build volume mb = MeshBuilder() mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color) self.setMeshData(mb.build()) # Build plate grid mesh mb = MeshBuilder() mb.addQuad( Vector(min_w, min_h - z_fight_distance, min_d), Vector(max_w, min_h - z_fight_distance, min_d), Vector(max_w, min_h - z_fight_distance, max_d), Vector(min_w, min_h - z_fight_distance, max_d) ) for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() else: # Bottom and top 'ellipse' of the build volume aspect = 1.0 scale_matrix = Matrix() if self._width != 0: # Scale circular meshes by aspect ratio if width != height aspect = self._depth / self._width scale_matrix.compose(scale = Vector(1, 1, aspect)) mb = MeshBuilder() mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color) mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color) self.setMeshData(mb.build().getTransformed(scale_matrix)) # Build plate grid mesh mb = MeshBuilder() mb.addVertex(0, min_h - z_fight_distance, 0) mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0)) sections = mb.getVertexCount() - 1 # Center point is not an arc section indices = [] for n in range(0, sections - 1): indices.append([0, n + 2, n + 1]) mb.addIndices(numpy.asarray(indices, dtype = numpy.int32)) mb.calculateNormals() for n in range(0, mb.getVertexCount()): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2] * aspect) self._grid_mesh = mb.build().getTransformed(scale_matrix) # Indication of the machine origin if self._global_container_stack.getProperty("machine_center_is_zero", "value"): origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2 else: origin = Vector(min_w, min_h, max_d) mb = MeshBuilder() mb.addCube( width = self._origin_line_length, height = self._origin_line_width, depth = self._origin_line_width, center = origin + Vector(self._origin_line_length / 2, 0, 0), color = self._x_axis_color ) mb.addCube( width = self._origin_line_width, height = self._origin_line_length, depth = self._origin_line_width, center = origin + Vector(0, self._origin_line_length / 2, 0), color = self._y_axis_color ) mb.addCube( width = self._origin_line_width, height = self._origin_line_width, depth = self._origin_line_length, center = origin - Vector(0, 0, self._origin_line_length / 2), color = self._z_axis_color ) self._origin_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: mb = MeshBuilder() color = self._disallowed_area_color for polygon in self._disallowed_areas: points = polygon.getPoints() if len(points) == 0: continue first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color = color) previous_point = new_point # Find the largest disallowed area to exclude it from the maximum scale bounds. # This is a very nasty hack. This pretty much only works for UM machines. # This disallowed area_size needs a -lot- of rework at some point in the future: TODO if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area. size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1])) else: size = 0 disallowed_area_size = max(size, disallowed_area_size) self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None if self._error_areas: mb = MeshBuilder() for error_area in self._error_areas: color = self._error_area_color points = error_area.getPoints() first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) for point in points: new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d)) mb.addFace(first, previous_point, new_point, color=color) previous_point = new_point self._error_mesh = mb.build() else: self._error_mesh = None self._volume_aabb = AxisAlignedBox( minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)) bed_adhesion_size = self._getEdgeDisallowedSize() # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! # The +1 and -1 is added as there is always a bit of extra room required to work properly. scale_to_max_bounds = AxisAlignedBox( minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) ) Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds def getBoundingBox(self): return self._volume_aabb def getRaftThickness(self): return self._raft_thickness def _updateRaftThickness(self): old_raft_thickness = self._raft_thickness self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value") self._raft_thickness = 0.0 if self._adhesion_type == "raft": self._raft_thickness = ( self._global_container_stack.getProperty("raft_base_thickness", "value") + self._global_container_stack.getProperty("raft_interface_thickness", "value") + self._global_container_stack.getProperty("raft_surface_layers", "value") * self._global_container_stack.getProperty("raft_surface_thickness", "value") + self._global_container_stack.getProperty("raft_airgap", "value")) # Rounding errors do not matter, we check if raft_thickness has changed at all if old_raft_thickness != self._raft_thickness: self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World) self.raftThicknessChanged.emit() def _updateExtraZClearance(self): extra_z = None extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) for extruder in extruders: retraction_hop = extruder.getProperty("retraction_hop", "value") if extra_z is None or retraction_hop > extra_z: extra_z = retraction_hop if extra_z is None: # If no extruders, take global value. extra_z = self._global_container_stack.getProperty("retraction_hop", "value") if extra_z != self._extra_z_clearance: self._extra_z_clearance = extra_z ## Update the build volume visualization def _onStackChanged(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack = Application.getInstance().getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) for extruder in extruders: extruder.propertyChanged.connect(self._onSettingPropertyChanged) self._width = self._global_container_stack.getProperty("machine_width", "value") machine_height = self._global_container_stack.getProperty("machine_height", "value") if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height) if self._height < machine_height: self._build_volume_message.show() else: self._build_volume_message.hide() else: self._height = self._global_container_stack.getProperty("machine_height", "value") self._build_volume_message.hide() self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._updateDisallowedAreas() self._updateRaftThickness() if self._engine_ready: self.rebuild() def _onEngineCreated(self): self._engine_ready = True self.rebuild() def _onSettingPropertyChanged(self, setting_key, property_name): if property_name != "value": return rebuild_me = False if setting_key == "print_sequence": machine_height = self._global_container_stack.getProperty("machine_height", "value") if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height) if self._height < machine_height: self._build_volume_message.show() else: self._build_volume_message.hide() else: self._height = self._global_container_stack.getProperty("machine_height", "value") self._build_volume_message.hide() rebuild_me = True if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings: self._updateDisallowedAreas() rebuild_me = True if setting_key in self._raft_settings: self._updateRaftThickness() rebuild_me = True if setting_key in self._extra_z_settings: self._updateExtraZClearance() rebuild_me = True if rebuild_me: self.rebuild() def hasErrors(self): return self._has_errors ## Calls _updateDisallowedAreas and makes sure the changes appear in the # scene. # # This is required for a signal to trigger the update in one go. The # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, # since there may be other changes before it needs to be rebuilt, which # would hit performance. def _updateDisallowedAreasAndRebuild(self): self._updateDisallowedAreas() self.rebuild() def _updateDisallowedAreas(self): if not self._global_container_stack: return self._error_areas = [] extruder_manager = ExtruderManager.getInstance() used_extruders = extruder_manager.getUsedExtruderStacks() disallowed_border_size = self._getEdgeDisallowedSize() if not used_extruders: # If no extruder is used, assume that the active extruder is used (else nothing is drawn) if extruder_manager.getActiveExtruderStack(): used_extruders = [extruder_manager.getActiveExtruderStack()] else: used_extruders = [self._global_container_stack] result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added. prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders) prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. #Check if prime positions intersect with disallowed areas. for extruder in used_extruders: extruder_id = extruder.getId() collision = False for prime_polygon in prime_areas[extruder_id]: for disallowed_polygon in prime_disallowed_areas[extruder_id]: if prime_polygon.intersectsPolygon(disallowed_polygon) is not None: collision = True break if collision: break #Also check other prime positions (without additional offset). for other_extruder_id in prime_areas: if extruder_id == other_extruder_id: #It is allowed to collide with itself. continue for other_prime_polygon in prime_areas[other_extruder_id]: if prime_polygon.intersectsPolygon(other_prime_polygon): collision = True break if collision: break if collision: break result_areas[extruder_id].extend(prime_areas[extruder_id]) nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value") for area in nozzle_disallowed_areas: polygon = Polygon(numpy.array(area, numpy.float32)) polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) result_areas[extruder_id].append(polygon) #Don't perform the offset on these. # Add prime tower location as disallowed area. prime_tower_collision = False prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) for extruder_id in prime_tower_areas: for prime_tower_area in prime_tower_areas[extruder_id]: for area in result_areas[extruder_id]: if prime_tower_area.intersectsPolygon(area) is not None: prime_tower_collision = True break if prime_tower_collision: #Already found a collision. break if not prime_tower_collision: result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) else: self._error_areas.extend(prime_tower_areas[extruder_id]) self._has_errors = len(self._error_areas) > 0 self._disallowed_areas = [] for extruder_id in result_areas: self._disallowed_areas.extend(result_areas[extruder_id]) ## Computes the disallowed areas for objects that are printed with print # features. # # This means that the brim, travel avoidance and such will be applied to # these features. # # \return A dictionary with for each used extruder ID the disallowed areas # where that extruder may not print. def _computeDisallowedAreasPrinted(self, used_extruders): result = {} for extruder in used_extruders: result[extruder.getId()] = [] #Currently, the only normally printed object is the prime tower. if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable") == True: prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value") prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") if not self._global_container_stack.getProperty("machine_center_is_zero", "value"): prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_y = prime_tower_y + machine_depth / 2 prime_tower_area = Polygon([ [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y], [prime_tower_x - prime_tower_size, prime_tower_y], ]) prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0)) for extruder in used_extruders: result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset. return result ## Computes the disallowed areas for the prime locations. # # These are special because they are not subject to things like brim or # travel avoidance. They do get a dilute with the border size though # because they may not intersect with brims and such of other objects. # # \param border_size The size with which to offset the disallowed areas # due to skirt, brim, travel avoid distance, etc. # \param used_extruders The extruder stacks to generate disallowed areas # for. # \return A dictionary with for each used extruder ID the prime areas. def _computeDisallowedAreasPrime(self, border_size, used_extruders): result = {} machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value") for extruder in used_extruders: prime_x = extruder.getProperty("extruder_prime_pos_x", "value") prime_y = - extruder.getProperty("extruder_prime_pos_y", "value") #Ignore extruder prime position if it is not set if prime_x == 0 and prime_y == 0: result[extruder.getId()] = [] continue if not self._global_container_stack.getProperty("machine_center_is_zero", "value"): prime_x = prime_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_y = prime_x + machine_depth / 2 prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE) prime_polygon = prime_polygon.translate(prime_x, prime_y) prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size)) result[extruder.getId()] = [prime_polygon] return result ## Computes the disallowed areas that are statically placed in the machine. # # It computes different disallowed areas depending on the offset of the # extruder. The resulting dictionary will therefore have an entry for each # extruder that is used. # # \param border_size The size with which to offset the disallowed areas # due to skirt, brim, travel avoid distance, etc. # \param used_extruders The extruder stacks to generate disallowed areas # for. # \return A dictionary with for each used extruder ID the disallowed areas # where that extruder may not print. def _computeDisallowedAreasStatic(self, border_size, used_extruders): #Convert disallowed areas to polygons and dilate them. machine_disallowed_polygons = [] for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"): polygon = Polygon(numpy.array(area, numpy.float32)) polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size)) machine_disallowed_polygons.append(polygon) result = {} for extruder in used_extruders: extruder_id = extruder.getId() offset_x = extruder.getProperty("machine_nozzle_offset_x", "value") if offset_x is None: offset_x = 0 offset_y = extruder.getProperty("machine_nozzle_offset_y", "value") if offset_y is None: offset_y = 0 result[extruder_id] = [] for polygon in machine_disallowed_polygons: result[extruder_id].append(polygon.translate(offset_x, offset_y)) #Compensate for the nozzle offset of this extruder. #Add the border around the edge of the build volume. left_unreachable_border = 0 right_unreachable_border = 0 top_unreachable_border = 0 bottom_unreachable_border = 0 #The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders. for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value") other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value") left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x) right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x) top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y) bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y) half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2 half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2 if self._shape != "elliptic": if border_size - left_unreachable_border > 0: result[extruder_id].append(Polygon(numpy.array([ [-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border], [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border] ], numpy.float32))) if border_size + right_unreachable_border > 0: result[extruder_id].append(Polygon(numpy.array([ [half_machine_width, half_machine_depth], [half_machine_width, -half_machine_depth], [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border], [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border] ], numpy.float32))) if border_size + bottom_unreachable_border > 0: result[extruder_id].append(Polygon(numpy.array([ [-half_machine_width, half_machine_depth], [half_machine_width, half_machine_depth], [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border], [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border] ], numpy.float32))) if border_size - top_unreachable_border > 0: result[extruder_id].append(Polygon(numpy.array([ [half_machine_width, -half_machine_depth], [-half_machine_width, -half_machine_depth], [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border], [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border] ], numpy.float32))) else: sections = 32 arc_vertex = [0, half_machine_depth - border_size] for i in range(0, sections): quadrant = math.floor(4 * i / sections) vertices = [] if quadrant == 0: vertices.append([-half_machine_width, half_machine_depth]) elif quadrant == 1: vertices.append([-half_machine_width, -half_machine_depth]) elif quadrant == 2: vertices.append([half_machine_width, -half_machine_depth]) elif quadrant == 3: vertices.append([half_machine_width, half_machine_depth]) vertices.append(arc_vertex) angle = 2 * math.pi * (i + 1) / sections arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)] vertices.append(arc_vertex) result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32))) if border_size > 0: result[extruder_id].append(Polygon(numpy.array([ [-half_machine_width, -half_machine_depth], [-half_machine_width, half_machine_depth], [-half_machine_width + border_size, 0] ], numpy.float32))) result[extruder_id].append(Polygon(numpy.array([ [-half_machine_width, half_machine_depth], [ half_machine_width, half_machine_depth], [ 0, half_machine_depth - border_size] ], numpy.float32))) result[extruder_id].append(Polygon(numpy.array([ [ half_machine_width, half_machine_depth], [ half_machine_width, -half_machine_depth], [ half_machine_width - border_size, 0] ], numpy.float32))) result[extruder_id].append(Polygon(numpy.array([ [ half_machine_width,-half_machine_depth], [-half_machine_width,-half_machine_depth], [ 0, -half_machine_depth + border_size] ], numpy.float32))) return result ## Private convenience function to get a setting from the adhesion # extruder. # # \param setting_key The key of the setting to get. # \param property The property to get from the setting. # \return The property of the specified setting in the adhesion extruder. def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"): return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property) ## Private convenience function to get a setting from every extruder. # # For single extrusion machines, this gets the setting from the global # stack. # # \return A sequence of setting values, one for each extruder. def _getSettingFromAllExtruders(self, setting_key, property = "value"): all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property) all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") for i in range(len(all_values)): if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"): all_values[i] = 0 return all_values ## Private convenience function to get a setting from the support infill # extruder. # # \param setting_key The key of the setting to get. # \param property The property to get from the setting. # \return The property of the specified setting in the support infill # extruder. def _getSettingFromSupportInfillExtruder(self, setting_key, property = "value"): return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property) ## Helper function to get a setting from an extruder specified in another # setting. # # \param setting_key The key of the setting to get. # \param extruder_setting_key The key of the setting that specifies from # which extruder to get the setting, if there are multiple extruders. # \param property The property to get from the setting. # \return The property of the specified setting in the specified extruder. def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"): multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1 if not multi_extrusion: stack = self._global_container_stack else: extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value") if extruder_index == "-1": # If extruder index is -1 use global instead stack = self._global_container_stack else: extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)] stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] value = stack.getProperty(setting_key, property) setting_type = stack.getProperty(setting_key, "type") if not value and (setting_type == "int" or setting_type == "float"): return 0 return value ## Convenience function to calculate the disallowed radius around the edge. # # This disallowed radius is to allow for space around the models that is # not part of the collision radius, such as bed adhesion (skirt/brim/raft) # and travel avoid distance. def _getEdgeDisallowedSize(self): if not self._global_container_stack: return 0 container_stack = self._global_container_stack # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects if container_stack.getProperty("print_sequence", "value") == "one_at_a_time": return 0.1 # Return a very small value, so we do draw disallowed area's near the edges. adhesion_type = container_stack.getProperty("adhesion_type", "value") if adhesion_type == "skirt": skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap") skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count") bed_adhesion_size = skirt_distance + (skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width")) if len(ExtruderManager.getInstance().getUsedExtruderStacks()) > 1: adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")) extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width") del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr. for value in extruder_values: bed_adhesion_size += value elif adhesion_type == "brim": bed_adhesion_size = self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("skirt_brim_line_width") if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")) extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width") del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr. for value in extruder_values: bed_adhesion_size += value elif adhesion_type == "raft": bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin") elif adhesion_type == "none": bed_adhesion_size = 0 else: raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?") support_expansion = 0 if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"): support_expansion += self._getSettingFromSupportInfillExtruder("support_offset") farthest_shield_distance = 0 if container_stack.getProperty("draft_shield_enabled", "value"): farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value")) if container_stack.getProperty("ooze_shield_enabled", "value"): farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value")) move_from_wall_radius = 0 # Moves that start from outer wall. move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist"))) used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders] travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders] for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global). if avoid_other_parts_enabled: move_from_wall_radius = max(move_from_wall_radius, avoid_distance) # Now combine our different pieces of data to get the final border size. # Support expansion is added to the bed adhesion, since the bed adhesion goes around support. # Support expansion is added to farthest shield distance, since the shields go around support. border_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size) return border_size def _clamp(self, value, min_value, max_value): return max(min(value, max_value), min_value) _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist"] _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"] _extra_z_settings = ["retraction_hop"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"] _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"] _extruder_settings = ["support_enable", "support_interface_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_interface_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
def __init__(self, parent = None): super().__init__(parent) self._volume_outline_color = None self._x_axis_color = None self._y_axis_color = None self._z_axis_color = None self._disallowed_area_color = None self._error_area_color = None self._width = 0 self._height = 0 self._depth = 0 self._shape = "" self._shader = None self._origin_mesh = None self._origin_line_length = 20 self._origin_line_width = 0.5 self._grid_mesh = None self._grid_shader = None self._disallowed_areas = [] self._disallowed_area_mesh = None self._error_areas = [] self._error_mesh = None self.setCalculateBoundingBox(False) self._volume_aabb = None self._raft_thickness = 0.0 self._extra_z_clearance = 0.0 self._adhesion_type = None self._platform = Platform(self) self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged) self._onStackChanged() self._engine_ready = False Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) self._has_errors = False Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged) #Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() self._change_timer = QTimer() self._change_timer.setInterval(100) self._change_timer.setSingleShot(True) self._change_timer.timeout.connect(self._onChangeTimerFinished) self._build_volume_message = Message(catalog.i18nc("@info:status", "The build volume height has been reduced due to the value of the" " \"Print Sequence\" setting to prevent the gantry from colliding" " with printed models.")) # Must be after setting _build_volume_message, apparently that is used in getMachineManager. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # Therefore this works. Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged) # This should also ways work, and it is semantically more correct, # but it does not update the disallowed areas after material change Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
class CuraEngineBackend(QObject, Backend): ## 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, parent = None): super().__init__(parent = parent) # 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" if Platform.isWindows(): executable_name += ".exe" default_engine_location = executable_name if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)): default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name) if hasattr(sys, "frozen"): default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name) if Platform.isLinux() and not default_engine_location: if not os.getenv("PATH"): raise OSError("There is something wrong with your Linux installation.") for pathdir in os.getenv("PATH").split(os.pathsep): execpath = os.path.join(pathdir, executable_name) if os.path.exists(execpath): default_engine_location = execpath break if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") 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) self._onActiveViewChanged() self._stored_layer_data = [] self._stored_optimized_layer_data = [] self._scene = Application.getInstance().getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for when to (re)start slicing: self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() self._active_extruder_stack = None ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged) self._onActiveExtruderChanged() # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage self._message_handlers["cura.proto.Progress"] = self._onProgressMessage self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._start_slice_job = None self._slicing = False # Are we currently slicing? self._restart = False # Back-end is currently restarting? self._tool_active = False # If a tool is active, some tasks do not have to do anything self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. self._need_slicing = False self._engine_is_fresh = True # Is the newly started engine used before or not? self._backend_log_max_lines = 20000 # Maximum number of lines to buffer self._error_message = None # Pop-up message that shows errors. 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) self._use_timer = 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() self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) ## Terminate the engine process. # # This function should terminate the engine process. # Called when closing the application. def close(self): # 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): json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] ## 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. slicingStarted = Signal() ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() @pyqtSlot() def stopSlicing(self): self.backendStateChange.emit(BackendState.NotStarted) if self._slicing: # We were already slicing. Stop the old job. self._terminate() self._createSocket() if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon. self._process_layers_job.abort() self._process_layers_job = None if self._error_message: self._error_message.hide() ## Manually triggers a reslice @pyqtSlot() def forceSlice(self): if self._use_timer: self._change_timer.start() else: self.slice() ## Perform a slice of the scene. def slice(self): self._slice_start_time = time() if not self._need_slicing: self.processingProgress.emit(1.0) self.backendStateChange.emit(BackendState.Done) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") return self.printDurationMessage.emit(0, [0]) self._stored_layer_data = [] self._stored_optimized_layer_data = [] if self._process is None: self._createSocket() self.stopSlicing() self._engine_is_fresh = False # Yes we're going to use the engine self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) self._scene.gcode_list = [] self._slicing = True self.slicingStarted.emit() slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) 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): self._slicing = False self._stored_layer_data = [] self._stored_optimized_layer_data = [] if self._start_slice_job is not None: self._start_slice_job.cancel() self.slicingCancelled.emit() self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") if Application.getInstance().getCommandLineOption("external-backend", False): return if self._process is not None: Logger.log("d", "Killing engine process") try: self._process.terminate() Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) self._process = None 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): if self._error_message: self._error_message.hide() # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: return if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if Application.getInstance().platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "The selected material is incompatible with the selected machine or configuration.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.SettingError: if Application.getInstance().platformActivity: extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) error_keys = [] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: error_keys = self._global_container_stack.getErrorKeys() error_labels = set() definition_container = self._global_container_stack.getBottom() for key in error_keys: error_labels.add(definition_container.findDefinitions(key = key)[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.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: if Application.getInstance().platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if Application.getInstance().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.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return # 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 self.backendStateChange.emit(BackendState.Processing) 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): enable_timer = True if not Preferences.getInstance().getValue("general/auto_slice"): enable_timer = False for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isBlockSlicing"): enable_timer = False self.backendStateChange.emit(BackendState.Disabled) gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: self._scene.gcode_list = gcode_list if self._use_timer == enable_timer: return self._use_timer if enable_timer: self.backendStateChange.emit(BackendState.NotStarted) self.enableTimer() return True else: self.disableTimer() return False ## 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): if self._tool_active: return if type(source) is not SceneNode: return if source is self._scene.getRoot(): return self.determineAutoSlicing() if source.getMeshData() is None: return if source.getMeshData().getVertices() is None: return self.needsSlicing() self.stopSlicing() self._onChanged() ## Called when an error occurs in the socket connection towards the engine. # # \param error The exception that occurred. def _onSocketError(self, error): if Application.getInstance().isShuttingDown(): return super()._onSocketError(error) if error.getErrorCode() == Arcus.ErrorCode.Debug: return self._terminate() self._createSocket() 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") ## Remove old layer data (if any) def _clearLayerData(self): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break ## Convenient function: set need_slicing, emit state and clear layer data def needsSlicing(self): self._need_slicing = True self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) if not self._use_timer: # 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, property): if property == "value": # Only reslice if the value has changed. 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): 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): self._stored_optimized_layer_data.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): self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) ## 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): self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) self._slicing = False self._need_slicing = False Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()): self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() self._stored_optimized_layer_data = [] ## 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): self._scene.gcode_list.append(message.data.decode("utf-8", "replace")) ## 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): self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace")) ## Called when a print time message is received from the engine. # # \param message The protobuff message containing the print time and # material amount per extruder def _onPrintTimeMaterialEstimates(self, message): material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) self.printDurationMessage.emit(message.time, material_amounts) ## Creates a new socket connection. def _createSocket(self): super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto"))) 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, **kwargs): self.needsSlicing() if self._use_timer: self._change_timer.start() ## Called when the back-end connects to the front-end. def _onBackendConnected(self): 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): 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 if not self._engine_is_fresh: 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): self._tool_active = False # React on scene change again self.determineAutoSlicing() ## Called when the user changes the active view mode. def _onActiveViewChanged(self): if Application.getInstance().getController().getActiveView(): view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": # 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. if self._stored_optimized_layer_data and not self._slicing: self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() self._stored_optimized_layer_data = [] 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): if not self._restart: if self._process: Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) self._process = None ## Called when the global container stack changes def _onGlobalStackChanged(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) if extruders: for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack = Application.getInstance().getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. self._global_container_stack.containersChanged.connect(self._onChanged) extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) if extruders: for extruder in extruders: extruder.propertyChanged.connect(self._onSettingChanged) self._onActiveExtruderChanged() self._onChanged() def _onActiveExtruderChanged(self): if self._global_container_stack: # Connect all extruders of the active machine. This might cause a few connects that have already happend, # but that shouldn't cause issues as only new / unique connections are added. extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) if extruders: for extruder in extruders: extruder.propertyChanged.connect(self._onSettingChanged) if self._active_extruder_stack: self._active_extruder_stack.containersChanged.disconnect(self._onChanged) self._active_extruder_stack = ExtruderManager.getInstance().getActiveExtruderStack() if self._active_extruder_stack: self._active_extruder_stack.containersChanged.connect(self._onChanged) def _onProcessLayersFinished(self, job): self._process_layers_job = None ## Connect slice function to timer. def enableTimer(self): 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): if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) def _onPreferencesChanged(self, preference): if preference != "general/auto_slice": return auto_slice = self.determineAutoSlicing() if auto_slice: self._change_timer.start() ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self): if self._use_timer: self._change_timer.start()
def _onStartSliceCompleted(self, job): if self._error_message: self._error_message.hide() # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: return if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if Application.getInstance().platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "The selected material is incompatible with the selected machine or configuration.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.SettingError: if Application.getInstance().platformActivity: extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) error_keys = [] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: error_keys = self._global_container_stack.getErrorKeys() error_labels = set() definition_container = self._global_container_stack.getBottom() for key in error_keys: error_labels.add(definition_container.findDefinitions(key = key)[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.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: if Application.getInstance().platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if Application.getInstance().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.")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) else: self.backendStateChange.emit(BackendState.NotStarted) return # 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 self.backendStateChange.emit(BackendState.Processing) Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
class RotateTool(Tool): """Provides the tool to rotate meshes and groups The tool exposes a ToolHint to show the rotation angle of the current operation """ def __init__(self): super().__init__() self._handle = RotateToolHandle.RotateToolHandle() self._snap_rotation = True self._snap_angle = math.radians(15) self._angle = None self._angle_update_time = None self._shortcut_key = Qt.Key_R self._progress_message = None self._iterations = 0 self._total_iterations = 0 self._rotating = False self.setExposedProperties("ToolHint", "RotationSnap", "RotationSnapAngle", "SelectFaceSupported", "SelectFaceToLayFlatMode") self._saved_node_positions = [] self._select_face_mode = False Selection.selectedFaceChanged.connect(self._onSelectedFaceChanged) def event(self, event): """Handle mouse and keyboard events :param event: type(Event) """ super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: # Snap is toggled when pressing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled( ): # Start a rotate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if self._handle.isAxis(id): self.setLockedAxis(id) else: # Not clicked on an axis: do nothing. return False handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in self._getSelectedObjectsWithoutSelectedAncestors(): self._saved_node_positions.append((node, node.getPosition())) if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) else: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) self.setDragStart(event.x, event.y) self._rotating = False self._angle = 0 return True if event.type == Event.MouseMoveEvent: # Perform a rotate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) if not self.getDragStart(): #May have set it to None. return False if not self._rotating: self._rotating = True self.operationStarted.emit(self) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalized() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return False drag_end = (drag_position - handle_position).normalized() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return False rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) else: direction = -1 # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time self._angle += direction * angle self.propertyChanged.emit() # Rotate around the saved centeres of all selected nodes if len(self._saved_node_positions) > 1: op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation( RotateOperation(node, rotation, rotate_around_point=position)) op.push() else: for node, position in self._saved_node_positions: RotateOperation(node, rotation, rotate_around_point=position).push() self.setDragStart(event.x, event.y) return True if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(ToolHandle.NoAxis) self._angle = None self.propertyChanged.emit() if self._rotating: self.operationStopped.emit(self) return True def _onSelectedFaceChanged(self): if not self._select_face_mode: return self._handle.setEnabled(not Selection.getFaceSelectMode()) selected_face = Selection.getSelectedFace() if not Selection.getSelectedFace() or not ( Selection.hasSelection() and Selection.getFaceSelectMode()): return original_node, face_id = selected_face meshdata = original_node.getMeshDataTransformed() if not meshdata or face_id < 0: return if face_id > (meshdata.getVertexCount() / 3 if not meshdata.hasIndices() else meshdata.getFaceCount()): return face_mid, face_normal = meshdata.getFacePlane(face_id) object_mid = original_node.getBoundingBox().center rotation_point_vector = Vector(object_mid.x, object_mid.y, face_mid[2]) face_normal_vector = Vector(face_normal[0], face_normal[1], face_normal[2]) rotation_quaternion = Quaternion.rotationTo( face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0)) operation = GroupedOperation() current_node = None # type: Optional[SceneNode] for node in Selection.getAllSelectedObjects(): current_node = node parent_node = current_node.getParent() while parent_node and parent_node.callDecoration("isGroup"): current_node = parent_node parent_node = current_node.getParent() if current_node is None: return rotate_operation = RotateOperation(current_node, rotation_quaternion, rotation_point_vector) operation.addOperation(rotate_operation) operation.push() # NOTE: We might want to consider unchecking the select-face button afterthe operation is done. def getToolHint(self): """Return a formatted angle of the current rotate operation :return: type(String) fully formatted string showing the angle by which the mesh(es) are rotated """ return "%d°" % round(math.degrees( self._angle)) if self._angle else None def getSelectFaceSupported(self) -> bool: """Get whether the select face feature is supported. :return: True if it is supported, or False otherwise. """ # Use a dummy postfix, since an equal version with a postfix is considered smaller normally. return Version(OpenGL.getInstance().getOpenGLVersion()) >= Version( "4.1 dummy-postfix") def getRotationSnap(self): """Get the state of the "snap rotation to N-degree increments" option :return: type(Boolean) """ return self._snap_rotation def setRotationSnap(self, snap): """Set the state of the "snap rotation to N-degree increments" option :param snap: type(Boolean) """ if snap != self._snap_rotation: self._snap_rotation = snap self.propertyChanged.emit() def getRotationSnapAngle(self): """Get the number of degrees used in the "snap rotation to N-degree increments" option""" return self._snap_angle def setRotationSnapAngle(self, angle): """Set the number of degrees used in the "snap rotation to N-degree increments" option""" if angle != self._snap_angle: self._snap_angle = angle self.propertyChanged.emit() def getSelectFaceToLayFlatMode(self) -> bool: """Whether the rotate tool is in 'Lay flat by face'-Mode.""" if not Selection.getFaceSelectMode(): self._select_face_mode = False # .. but not the other way around! return self._select_face_mode def setSelectFaceToLayFlatMode(self, select: bool) -> None: """Set the rotate tool to/from 'Lay flat by face'-Mode.""" if select != self._select_face_mode or select != Selection.getFaceSelectMode( ): self._select_face_mode = select if not select: Selection.clearFace() Selection.setFaceSelectMode(self._select_face_mode) self.propertyChanged.emit() def resetRotation(self): """Reset the orientation of the mesh(es) to their original orientation(s)""" for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.setMirror(Vector(1, 1, 1)) Selection.applyOperation(SetTransformOperation, None, Quaternion(), None) def layFlat(self): """Initialise and start a LayFlatOperation Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project """ self.operationStarted.emit(self) self._progress_message = Message( i18n_catalog.i18nc("@label", "Laying object flat on buildplate..."), lifetime=0, dismissable=False, title=i18n_catalog.i18nc("@title", "Object Rotation")) self._progress_message.setProgress(0) self._iterations = 0 self._total_iterations = 0 for selected_object in self._getSelectedObjectsWithoutSelectedAncestors( ): self._layObjectFlat(selected_object) self._progress_message.show() operations = Selection.applyOperation(LayFlatOperation) for op in operations: op.progress.connect(self._layFlatProgress) job = LayFlatJob(operations) job.finished.connect(self._layFlatFinished) job.start() def _layObjectFlat(self, selected_object): """Lays the given object flat. The given object can be a group or not.""" if not selected_object.callDecoration("isGroup"): self._total_iterations += selected_object.getMeshData( ).getVertexCount() * 2 else: for child in selected_object.getChildren(): self._layObjectFlat(child) def _layFlatProgress(self, iterations: int): """Called while performing the LayFlatOperation so progress can be shown Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications, :param iterations: type(int) number of iterations performed since the last callback """ self._iterations += iterations if self._progress_message: self._progress_message.setProgress( min(100 * (self._iterations / self._total_iterations), 100)) def _layFlatFinished(self, job): """Called when the LayFlatJob is done running all of its LayFlatOperations :param job: type(LayFlatJob) """ if self._progress_message: self._progress_message.hide() self._progress_message = None self.operationStopped.emit(self)
def _onStartSliceCompleted(self, job: StartSliceJob) -> None: if self._error_message: self._error_message.hide() # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error: self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartJobResult.SettingError: if self._application.platformActivity: if not self._global_container_stack: Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) error_keys = [] #type: 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. 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. Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key)) continue error_labels.add(definitions[0].label) 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) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) return elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} 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(): if not self._global_container_stack: Logger.log("e", "CuraEngineBackend does not have global_container_stack assigned.") continue 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 errors[key] = definition[0].label 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() == 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) 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() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) self._invokeSlice() return # 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 self.backendStateChange.emit(BackendState.Processing) if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
def on_connect_error(self, http_request, errorCode): message = Message(("Connection Failed!"), lifetime = 5) message.show() self.lb_printer_state.setProperty("text","Connection Failed!")
def _showMessage(self, message: str) -> None: """Show a UI message.""" Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: """Exports an profile to a file :param container_list: :type{list} the containers to export. This is not necessarily in any order! :param file_name: :type{str} the full path and filename to export to. :param file_type: :type{str} the file type with the format "<description> (*.<extension>)" :return: True if the export succeeded, false otherwise. """ # Parse the fileType to deduce what plugin can save the file format. # fileType has the format "<description> (*.<extension>)" split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. if split < 0: # Not found. Invalid format. Logger.log("e", "Invalid file format identifier %s", file_type) return False description = file_type[:split] extension = file_type[split + 4:-1] # Leave out the " (*." and ")". if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. file_name += "." + extension # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. if not Platform.isWindows(): if os.path.exists(file_name): result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name)) if result == QMessageBox.No: return False profile_writer = self._findProfileWriter(extension, description) try: if profile_writer is None: raise Exception("Unable to find a profile writer") success = profile_writer.write(file_name, container_list) except Exception as e: Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0, title = catalog.i18nc("@info:title", "Error")) m.show() return False if not success: Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0, title = catalog.i18nc("@info:title", "Error")) m.show() return False m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name), title = catalog.i18nc("@info:title", "Export succeeded")) m.show() return True
def startSendingThread(self): Logger.log('i', '=============QIDI SEND BEGIN============') self._errorMsg = '' self._qidi._abort = False self._stage = OutputStage.writing res = self._qidi.sendfile(self.targetSendFileName) if self._message: self._message.hide() self._message = None # type:Optional[Message] self.writeFinished.emit(self) self._stage = OutputStage.ready if res == QidiResult.SUCCES: if self._autoPrint is False: self._message = Message( catalog.i18nc("@info:status", "Do you wish to print now?"), title=catalog.i18nc("@label", "SUCCESS")) self._message.addAction("PRINT", catalog.i18nc("@action:button", "YES"), None, "") self._message.addAction("NO", catalog.i18nc("@action:button", "NO"), None, "") self._message.actionTriggered.connect(self._onActionTriggered) self._message.setProgress(None) self._message.show() else: self._onActionTriggered(self._message, "PRINT") self.writeSuccess.emit(self) self._stage = OutputStage.ready return self.writeError.emit(self) if res == QidiResult.ABORTED: Message(catalog.i18nc('@info:status', 'Upload Canceled'), title=catalog.i18nc("@info:title", "ABORTED")).show() return result_msg = "Unknown Error!!!" if self._result == QidiResult.TIMEOUT: result_msg = 'Connection timeout' elif self._result == QidiResult.WRITE_ERROR: self.writeError.emit(self) result_msg = self._errorMsg if 'create file' in self._errorMsg: m = Message(catalog.i18nc( '@info:status', ' Write error, please check that the SD card /U disk has been inserted' ), lifetime=0) m.show() elif self._result == QidiResult.FILE_EMPTY: self.writeError.emit(self) result_msg = 'File empty' elif self._result == QidiResult.FILE_NOT_OPEN: self.writeError.emit(self) result_msg = "Cannot Open File" self._message = Message(catalog.i18nc("@info:status", result_msg), title=catalog.i18nc("@label", "FAILURE")) self._message.show() Logger.log('e', result_msg)
def run(self): try: # Initialize a Preference that stores the last version checked for this printer. Application.getInstance().getPreferences().addPreference( getSettingsKeyForMachine(self._lookups.getMachineId()), "") # Get headers application_name = Application.getInstance().getApplicationName() application_version = Application.getInstance().getVersion() self._headers = { "User-Agent": "%s - %s" % (application_name, application_version) } # If it is not None, then we compare between the checked_version and the current_version machine_id = self._lookups.getMachineId() if machine_id is not None: Logger.log( "i", "You have a(n) {0} in the printer list. Do firmware-check." .format(self._machine_name)) current_version = self.getCurrentVersion() # This case indicates that was an error checking the version. # It happens for instance when not connected to internet. if current_version == self.ZERO_VERSION: return # If it is the first time the version is checked, the checked_version is "" setting_key_str = getSettingsKeyForMachine(machine_id) checked_version = Version( Application.getInstance().getPreferences().getValue( setting_key_str)) # If the checked_version is "", it's because is the first time we check firmware and in this case # we will not show the notification, but we will store it for the next time Application.getInstance().getPreferences().setValue( setting_key_str, current_version) Logger.log( "i", "Reading firmware version of %s: checked = %s - latest = %s", self._machine_name, checked_version, current_version) # The first time we want to store the current version, the notification will not be shown, # because the new version of Cura will be release before the firmware and we don't want to # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): Logger.log( "i", "Showing firmware update message for new version: {version}" .format(version=current_version)) message = FirmwareUpdateCheckerMessage( machine_id, self._machine_name, self._lookups.getRedirectUserUrl()) message.actionTriggered.connect(self._callback) message.show() else: Logger.log( "i", "No machine with name {0} in list of firmware to check.". format(self._machine_name)) except Exception as e: Logger.logException("w", "Failed to check for new version: %s", e) if not self.silent: Message( i18n_catalog.i18nc( "@info", "Could not access update information.")).show() return
class SimulationView(View): # Must match SimulationView.qml LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_LINE_TYPE = 1 LAYER_VIEW_TYPE_FEEDRATE = 2 LAYER_VIEW_TYPE_THICKNESS = 3 def __init__(self): super().__init__() self._max_layers = 0 self._current_layer_num = 0 self._minimum_layer_num = 0 self._current_layer_mesh = None self._current_layer_jumps = None self._top_layers_job = None self._activity = False self._old_max_layers = 0 self._max_paths = 0 self._current_path_num = 0 self._minimum_path_num = 0 self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged) self._busy = False self._simulation_running = False self._ghost_shader = None self._layer_pass = None self._composite_pass = None self._old_layer_bindings = None self._simulationview_composite_shader = None self._old_composite_shader = None self._global_container_stack = None self._proxy = SimulationViewProxy() self._controller.getScene().getRoot().childrenChanged.connect( self._onSceneChanged) self._resetSettings() self._legend_items = None self._show_travel_moves = False self._nozzle_node = None Preferences.getInstance().addPreference("view/top_layer_count", 5) Preferences.getInstance().addPreference("view/only_show_top_layers", False) Preferences.getInstance().addPreference( "view/force_layer_view_compatibility_mode", False) Preferences.getInstance().addPreference("layerview/layer_view_type", 0) Preferences.getInstance().addPreference("layerview/extruder_opacities", "") Preferences.getInstance().addPreference("layerview/show_travel_moves", False) Preferences.getInstance().addPreference("layerview/show_helpers", True) Preferences.getInstance().addPreference("layerview/show_skin", True) Preferences.getInstance().addPreference("layerview/show_infill", True) Preferences.getInstance().preferenceChanged.connect( self._onPreferencesChanged) self._updateWithPreferences() self._solid_layers = int( Preferences.getInstance().getValue("view/top_layer_count")) self._only_show_top_layers = bool( Preferences.getInstance().getValue("view/only_show_top_layers")) self._compatibility_mode = True # for safety self._wireprint_warning_message = Message(catalog.i18nc( "@info:status", "Cura does not accurately display layers when Wire Printing is enabled" ), title=catalog.i18nc( "@info:title", "Simulation View")) def _resetSettings(self): self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed self._extruder_count = 0 self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] self._show_travel_moves = 0 self._show_helpers = 1 self._show_skin = 1 self._show_infill = 1 self.resetLayerData() def getActivity(self): return self._activity def setActivity(self, activity): if self._activity == activity: return self._activity = activity self.activityChanged.emit() def getSimulationPass(self): if not self._layer_pass: # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._layer_pass = SimulationPass(1, 1) self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool( Preferences.getInstance().getValue( "view/force_layer_view_compatibility_mode")) self._layer_pass.setSimulationView(self) return self._layer_pass def getCurrentLayer(self): return self._current_layer_num def getMinimumLayer(self): return self._minimum_layer_num def getMaxLayers(self): return self._max_layers def getCurrentPath(self): return self._current_path_num def getMinimumPath(self): return self._minimum_path_num def getMaxPaths(self): return self._max_paths def getNozzleNode(self): if not self._nozzle_node: self._nozzle_node = NozzleNode() return self._nozzle_node def _onSceneChanged(self, node): self.setActivity(False) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self): return self._busy def setBusy(self, busy): if busy != self._busy: self._busy = busy self.busyChanged.emit() def isSimulationRunning(self): return self._simulation_running def setSimulationRunning(self, running): self._simulation_running = running def resetLayerData(self): self._current_layer_mesh = None self._current_layer_jumps = None self._max_feedrate = sys.float_info.min self._min_feedrate = sys.float_info.max self._max_thickness = sys.float_info.min self._min_thickness = sys.float_info.max def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._ghost_shader: self._ghost_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "color.shader")) self._ghost_shader.setUniformValue( "u_color", Color(*Application.getInstance().getTheme().getColor( "layerview_ghost").getRgb())) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected( node.getWatchedNode()): continue if not node.render(renderer): if (node.getMeshData()) and node.isVisible(): renderer.queueNode(node, transparent=True, shader=self._ghost_shader) def setLayer(self, value): if self._current_layer_num != value: self._current_layer_num = value if self._current_layer_num < 0: self._current_layer_num = 0 if self._current_layer_num > self._max_layers: self._current_layer_num = self._max_layers if self._current_layer_num < self._minimum_layer_num: self._minimum_layer_num = self._current_layer_num self._startUpdateTopLayers() self.currentLayerNumChanged.emit() def setMinimumLayer(self, value): if self._minimum_layer_num != value: self._minimum_layer_num = value if self._minimum_layer_num < 0: self._minimum_layer_num = 0 if self._minimum_layer_num > self._max_layers: self._minimum_layer_num = self._max_layers if self._minimum_layer_num > self._current_layer_num: self._current_layer_num = self._minimum_layer_num self._startUpdateTopLayers() self.currentLayerNumChanged.emit() def setPath(self, value): if self._current_path_num != value: self._current_path_num = value if self._current_path_num < 0: self._current_path_num = 0 if self._current_path_num > self._max_paths: self._current_path_num = self._max_paths if self._current_path_num < self._minimum_path_num: self._minimum_path_num = self._current_path_num self._startUpdateTopLayers() self.currentPathNumChanged.emit() def setMinimumPath(self, value): if self._minimum_path_num != value: self._minimum_path_num = value if self._minimum_path_num < 0: self._minimum_path_num = 0 if self._minimum_path_num > self._max_layers: self._minimum_path_num = self._max_layers if self._minimum_path_num > self._current_path_num: self._current_path_num = self._minimum_path_num self._startUpdateTopLayers() self.currentPathNumChanged.emit() ## Set the layer view type # # \param layer_view_type integer as in SimulationView.qml and this class def setSimulationViewType(self, layer_view_type): self._layer_view_type = layer_view_type self.currentLayerNumChanged.emit() ## Return the layer view type, integer as in SimulationView.qml and this class def getSimulationViewType(self): return self._layer_view_type ## Set the extruder opacity # # \param extruder_nr 0..3 # \param opacity 0.0 .. 1.0 def setExtruderOpacity(self, extruder_nr, opacity): if 0 <= extruder_nr <= 3: self._extruder_opacity[extruder_nr] = opacity self.currentLayerNumChanged.emit() def getExtruderOpacities(self): return self._extruder_opacity def setShowTravelMoves(self, show): self._show_travel_moves = show self.currentLayerNumChanged.emit() def getShowTravelMoves(self): return self._show_travel_moves def setShowHelpers(self, show): self._show_helpers = show self.currentLayerNumChanged.emit() def getShowHelpers(self): return self._show_helpers def setShowSkin(self, show): self._show_skin = show self.currentLayerNumChanged.emit() def getShowSkin(self): return self._show_skin def setShowInfill(self, show): self._show_infill = show self.currentLayerNumChanged.emit() def getShowInfill(self): return self._show_infill def getCompatibilityMode(self): return self._compatibility_mode def getExtruderCount(self): return self._extruder_count def getMinFeedrate(self): return self._min_feedrate def getMaxFeedrate(self): return self._max_feedrate def getMinThickness(self): return self._min_thickness def getMaxThickness(self): return self._max_thickness def calculateMaxLayers(self): scene = self.getController().getScene() self._old_max_layers = self._max_layers ## Recalculate num max layers new_max_layers = 0 for node in DepthFirstIterator(scene.getRoot()): layer_data = node.callDecoration("getLayerData") if not layer_data: continue self.setActivity(True) min_layer_number = sys.maxsize max_layer_number = -sys.maxsize for layer_id in layer_data.getLayers(): # Store the max and min feedrates and thicknesses for display purposes for p in layer_data.getLayer(layer_id).polygons: self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate) self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate) self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness) self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness) if max_layer_number < layer_id: max_layer_number = layer_id if min_layer_number > layer_id: min_layer_number = layer_id layer_count = max_layer_number - min_layer_number if new_max_layers < layer_count: new_max_layers = layer_count if new_max_layers > 0 and new_max_layers != self._old_max_layers: self._max_layers = new_max_layers # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first # if it's the largest value. If we don't do this, we can have a slider block outside of the # slider. if new_max_layers > self._current_layer_num: self.maxLayersChanged.emit() self.setLayer(int(self._max_layers)) else: self.setLayer(int(self._max_layers)) self.maxLayersChanged.emit() self._startUpdateTopLayers() def calculateMaxPathsOnLayer(self, layer_num): # Update the currentPath scene = self.getController().getScene() for node in DepthFirstIterator(scene.getRoot()): layer_data = node.callDecoration("getLayerData") if not layer_data: continue layer = layer_data.getLayer(layer_num) if layer is None: return new_max_paths = layer.lineMeshElementCount() if new_max_paths >= 0 and new_max_paths != self._max_paths: self._max_paths = new_max_paths self.maxPathsChanged.emit() self.setPath(int(new_max_paths)) maxLayersChanged = Signal() maxPathsChanged = Signal() currentLayerNumChanged = Signal() currentPathNumChanged = Signal() globalStackChanged = Signal() preferencesChanged = Signal() busyChanged = Signal() activityChanged = Signal() ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): return self._proxy def endRendering(self): pass def event(self, event): modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier if event.type == Event.KeyPressEvent and ctrl_is_active: amount = 10 if shift_is_active else 1 if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + amount) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - amount) return True if event.type == Event.ViewActivateEvent: # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() self.getRenderer().addRenderPass(layer_pass) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() nozzle.setParent(self.getController().getScene().getRoot()) nozzle.setVisible(False) Application.getInstance().globalContainerStackChanged.connect( self._onGlobalStackChanged) self._onGlobalStackChanged() if not self._simulationview_composite_shader: self._simulationview_composite_shader = OpenGL.getInstance( ).createShaderProgram( os.path.join( PluginRegistry.getInstance().getPluginPath( "SimulationView"), "simulationview_composite.shader")) theme = Application.getInstance().getTheme() self._simulationview_composite_shader.setUniformValue( "u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._simulationview_composite_shader.setUniformValue( "u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) if not self._composite_pass: self._composite_pass = self.getRenderer().getRenderPass( "composite") self._old_layer_bindings = self._composite_pass.getLayerBindings( )[:] # make a copy so we can restore to it later self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader( ) self._composite_pass.setCompositeShader( self._simulationview_composite_shader) elif event.type == Event.ViewDeactivateEvent: self._wireprint_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect( self._onGlobalStackChanged) if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onPropertyChanged) self._nozzle_node.setParent(None) self.getRenderer().removeRenderPass(self._layer_pass) self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setCompositeShader(self._old_composite_shader) def getCurrentLayerMesh(self): return self._current_layer_mesh def getCurrentLayerJumps(self): return self._current_layer_jumps def _onGlobalStackChanged(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onPropertyChanged) self._global_container_stack = Application.getInstance( ).getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect( self._onPropertyChanged) self._extruder_count = self._global_container_stack.getProperty( "machine_extruder_count", "value") self._onPropertyChanged("wireframe_enabled", "value") self.globalStackChanged.emit() else: self._wireprint_warning_message.hide() def _onPropertyChanged(self, key, property_name): if key == "wireframe_enabled" and property_name == "value": if self._global_container_stack.getProperty( "wireframe_enabled", "value"): self._wireprint_warning_message.show() else: self._wireprint_warning_message.hide() def _onCurrentLayerNumChanged(self): self.calculateMaxPathsOnLayer(self._current_layer_num) def _startUpdateTopLayers(self): if not self._compatibility_mode: return if self._top_layers_job: self._top_layers_job.finished.disconnect( self._updateCurrentLayerMesh) self._top_layers_job.cancel() self.setBusy(True) self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) self._top_layers_job.start() def _updateCurrentLayerMesh(self, job): self.setBusy(False) if not job.getResult(): return self.resetLayerData( ) # Reset the layer data only when job is done. Doing it now prevents "blinking" data. self._current_layer_mesh = job.getResult().get("layers") if self._show_travel_moves: self._current_layer_jumps = job.getResult().get("jumps") self._controller.getScene().sceneChanged.emit( self._controller.getScene().getRoot()) self._top_layers_job = None def _updateWithPreferences(self): self._solid_layers = int( Preferences.getInstance().getValue("view/top_layer_count")) self._only_show_top_layers = bool( Preferences.getInstance().getValue("view/only_show_top_layers")) self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool( Preferences.getInstance().getValue( "view/force_layer_view_compatibility_mode")) self.setSimulationViewType( int( float(Preferences.getInstance().getValue( "layerview/layer_view_type")))) for extruder_nr, extruder_opacity in enumerate( Preferences.getInstance().getValue( "layerview/extruder_opacities").split("|")): try: opacity = float(extruder_opacity) except ValueError: opacity = 1.0 self.setExtruderOpacity(extruder_nr, opacity) self.setShowTravelMoves( bool(Preferences.getInstance().getValue( "layerview/show_travel_moves"))) self.setShowHelpers( bool(Preferences.getInstance().getValue("layerview/show_helpers"))) self.setShowSkin( bool(Preferences.getInstance().getValue("layerview/show_skin"))) self.setShowInfill( bool(Preferences.getInstance().getValue("layerview/show_infill"))) self._startUpdateTopLayers() self.preferencesChanged.emit() def _onPreferencesChanged(self, preference): if preference not in { "view/top_layer_count", "view/only_show_top_layers", "view/force_layer_view_compatibility_mode", "layerview/layer_view_type", "layerview/extruder_opacities", "layerview/show_travel_moves", "layerview/show_helpers", "layerview/show_skin", "layerview/show_infill", }: return self._updateWithPreferences()
def __init__(self, parent = None) -> None: super().__init__(parent) self._max_layers = 0 self._current_layer_num = 0 self._minimum_layer_num = 0 self._current_layer_mesh = None self._current_layer_jumps = None self._top_layers_job = None # type: Optional["_CreateTopLayersJob"] self._activity = False self._old_max_layers = 0 self._max_paths = 0 self._current_path_num = 0 self._minimum_path_num = 0 self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged) self._busy = False self._simulation_running = False self._ghost_shader = None # type: Optional["ShaderProgram"] self._layer_pass = None # type: Optional[SimulationPass] self._composite_pass = None # type: Optional[CompositePass] self._old_layer_bindings = None # type: Optional[List[str]] self._simulationview_composite_shader = None # type: Optional["ShaderProgram"] self._old_composite_shader = None # type: Optional["ShaderProgram"] self._max_feedrate = sys.float_info.min self._min_feedrate = sys.float_info.max self._max_thickness = sys.float_info.min self._min_thickness = sys.float_info.max self._global_container_stack = None # type: Optional[ContainerStack] self._proxy = None self._resetSettings() self._legend_items = None self._show_travel_moves = False self._nozzle_node = None # type: Optional[NozzleNode] Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5) Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False) Application.getInstance().getPreferences().addPreference("view/force_layer_view_compatibility_mode", False) Application.getInstance().getPreferences().addPreference("layerview/layer_view_type", 0) Application.getInstance().getPreferences().addPreference("layerview/extruder_opacities", "") Application.getInstance().getPreferences().addPreference("layerview/show_travel_moves", False) Application.getInstance().getPreferences().addPreference("layerview/show_helpers", True) Application.getInstance().getPreferences().addPreference("layerview/show_skin", True) Application.getInstance().getPreferences().addPreference("layerview/show_infill", True) self._updateWithPreferences() self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._compatibility_mode = self._evaluateCompatibilityMode() self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), title = catalog.i18nc("@info:title", "Simulation View")) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
def run(self): if not self._url: Logger.log("e", "Can not check for a new release. URL not set!") no_new_version = True application_name = Application.getInstance().getApplicationName() Logger.log("i", "Checking for new version of %s" % application_name) try: headers = { "User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion()) } # CURA-6698 Create an SSL context and use certifi CA certificates for verification. context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) context.verify_mode = ssl.CERT_REQUIRED context.load_verify_locations(cafile=certifi.where()) request = urllib.request.Request(self._url, headers=headers) latest_version_file = urllib.request.urlopen(request, context=context) except Exception as e: Logger.logException("w", "Failed to check for new version: %s" % e) if not self.silent: Message(i18n_catalog.i18nc( "@info", "Could not access update information."), title=i18n_catalog.i18nc("@info:title", "Version Upgrade")).show() return try: reader = codecs.getreader("utf-8") data = json.load(reader(latest_version_file)) try: if Application.getInstance().getVersion() != "master": local_version = Version( Application.getInstance().getVersion()) else: if not self.silent: Message(i18n_catalog.i18nc( "@info", "The version you are using does not support checking for updates." ), title=i18n_catalog.i18nc( "@info:title", "Warning")).show() return except ValueError: Logger.log( "w", "Could not determine application version from string %s, not checking for updates", Application.getInstance().getVersion()) if not self.silent: Message(i18n_catalog.i18nc( "@info", "The version you are using does not support checking for updates." ), title=i18n_catalog.i18nc( "@info:title", "Version Upgrade")).show() return if application_name in data: for key, value in data[application_name].items(): if "major" in value and "minor" in value and "revision" in value and "url" in value: os = key if platform.system().lower() == os.lower( ): #TODO: add architecture check newest_version = Version([ int(value["major"]), int(value["minor"]), int(value["revision"]) ]) if local_version < newest_version: preferences = Application.getInstance( ).getPreferences() latest_version_shown = preferences.getValue( "info/latest_update_version_shown") if latest_version_shown == newest_version and not self.display_same_version: continue # Don't show this update again. The user already clicked it away and doesn't want it again. preferences.setValue( "info/latest_update_version_shown", str(newest_version)) Logger.log( "i", "Found a new version of the software. Spawning message" ) self.showUpdate(newest_version, value["url"]) no_new_version = False break else: Logger.log( "w", "Could not find version information or download url for update." ) else: Logger.log( "w", "Did not find any version information for %s." % application_name) except Exception: Logger.logException( "e", "Exception in update checker while parsing the JSON file.") Message(i18n_catalog.i18nc( "@info", "An error occurred while checking for updates."), title=i18n_catalog.i18nc("@info:title", "Error")).show() no_new_version = False # Just to suppress the message below. if no_new_version and not self.silent: Message(i18n_catalog.i18nc("@info", "No new version was found."), title=i18n_catalog.i18nc("@info:title", "Version Upgrade")).show()
class SimulationView(CuraView): # Must match SimulationViewMenuComponent.qml LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_LINE_TYPE = 1 LAYER_VIEW_TYPE_FEEDRATE = 2 LAYER_VIEW_TYPE_THICKNESS = 3 def __init__(self, parent = None) -> None: super().__init__(parent) self._max_layers = 0 self._current_layer_num = 0 self._minimum_layer_num = 0 self._current_layer_mesh = None self._current_layer_jumps = None self._top_layers_job = None # type: Optional["_CreateTopLayersJob"] self._activity = False self._old_max_layers = 0 self._max_paths = 0 self._current_path_num = 0 self._minimum_path_num = 0 self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged) self._busy = False self._simulation_running = False self._ghost_shader = None # type: Optional["ShaderProgram"] self._layer_pass = None # type: Optional[SimulationPass] self._composite_pass = None # type: Optional[CompositePass] self._old_layer_bindings = None # type: Optional[List[str]] self._simulationview_composite_shader = None # type: Optional["ShaderProgram"] self._old_composite_shader = None # type: Optional["ShaderProgram"] self._max_feedrate = sys.float_info.min self._min_feedrate = sys.float_info.max self._max_thickness = sys.float_info.min self._min_thickness = sys.float_info.max self._global_container_stack = None # type: Optional[ContainerStack] self._proxy = None self._resetSettings() self._legend_items = None self._show_travel_moves = False self._nozzle_node = None # type: Optional[NozzleNode] Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5) Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False) Application.getInstance().getPreferences().addPreference("view/force_layer_view_compatibility_mode", False) Application.getInstance().getPreferences().addPreference("layerview/layer_view_type", 0) Application.getInstance().getPreferences().addPreference("layerview/extruder_opacities", "") Application.getInstance().getPreferences().addPreference("layerview/show_travel_moves", False) Application.getInstance().getPreferences().addPreference("layerview/show_helpers", True) Application.getInstance().getPreferences().addPreference("layerview/show_skin", True) Application.getInstance().getPreferences().addPreference("layerview/show_infill", True) self._updateWithPreferences() self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._compatibility_mode = self._evaluateCompatibilityMode() self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), title = catalog.i18nc("@info:title", "Simulation View")) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated) def _onEngineCreated(self) -> None: plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path: self.addDisplayComponent("main", os.path.join(plugin_path, "SimulationViewMainComponent.qml")) self.addDisplayComponent("menu", os.path.join(plugin_path, "SimulationViewMenuComponent.qml")) else: Logger.log("e", "Unable to find the path for %s", self.getPluginId()) def _evaluateCompatibilityMode(self) -> bool: return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")) def _resetSettings(self) -> None: self._layer_view_type = 0 # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness self._extruder_count = 0 self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] self._show_travel_moves = False self._show_helpers = True self._show_skin = True self._show_infill = True self.resetLayerData() def getActivity(self) -> bool: return self._activity def setActivity(self, activity: bool) -> None: if self._activity == activity: return self._activity = activity self.activityChanged.emit() def getSimulationPass(self) -> SimulationPass: if not self._layer_pass: # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._layer_pass = SimulationPass(1, 1) self._compatibility_mode = self._evaluateCompatibilityMode() self._layer_pass.setSimulationView(self) return self._layer_pass def getCurrentLayer(self) -> int: return self._current_layer_num def getMinimumLayer(self) -> int: return self._minimum_layer_num def getMaxLayers(self) -> int: return self._max_layers def getCurrentPath(self) -> int: return self._current_path_num def getMinimumPath(self) -> int: return self._minimum_path_num def getMaxPaths(self) -> int: return self._max_paths def getNozzleNode(self) -> NozzleNode: if not self._nozzle_node: self._nozzle_node = NozzleNode() return self._nozzle_node def _onSceneChanged(self, node: "SceneNode") -> None: if node.getMeshData() is None: return self.setActivity(False) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self) -> bool: return self._busy def setBusy(self, busy: bool) -> None: if busy != self._busy: self._busy = busy self.busyChanged.emit() def isSimulationRunning(self) -> bool: return self._simulation_running def setSimulationRunning(self, running: bool) -> None: self._simulation_running = running def resetLayerData(self) -> None: self._current_layer_mesh = None self._current_layer_jumps = None self._max_feedrate = sys.float_info.min self._min_feedrate = sys.float_info.max self._max_thickness = sys.float_info.min self._min_thickness = sys.float_info.max def beginRendering(self) -> None: scene = self.getController().getScene() renderer = self.getRenderer() if not self._ghost_shader: self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader")) theme = CuraApplication.getInstance().getTheme() if theme is not None: self._ghost_shader.setUniformValue("u_color", Color(*theme.getColor("layerview_ghost").getRgb())) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected(cast(ConvexHullNode, node).getWatchedNode()): continue if not node.render(renderer): if (node.getMeshData()) and node.isVisible(): renderer.queueNode(node, transparent = True, shader = self._ghost_shader) def setLayer(self, value: int) -> None: if self._current_layer_num != value: self._current_layer_num = value if self._current_layer_num < 0: self._current_layer_num = 0 if self._current_layer_num > self._max_layers: self._current_layer_num = self._max_layers if self._current_layer_num < self._minimum_layer_num: self._minimum_layer_num = self._current_layer_num self._startUpdateTopLayers() self.currentLayerNumChanged.emit() def setMinimumLayer(self, value: int) -> None: if self._minimum_layer_num != value: self._minimum_layer_num = value if self._minimum_layer_num < 0: self._minimum_layer_num = 0 if self._minimum_layer_num > self._max_layers: self._minimum_layer_num = self._max_layers if self._minimum_layer_num > self._current_layer_num: self._current_layer_num = self._minimum_layer_num self._startUpdateTopLayers() self.currentLayerNumChanged.emit() def setPath(self, value: int) -> None: if self._current_path_num != value: self._current_path_num = value if self._current_path_num < 0: self._current_path_num = 0 if self._current_path_num > self._max_paths: self._current_path_num = self._max_paths if self._current_path_num < self._minimum_path_num: self._minimum_path_num = self._current_path_num self._startUpdateTopLayers() self.currentPathNumChanged.emit() def setMinimumPath(self, value: int) -> None: if self._minimum_path_num != value: self._minimum_path_num = value if self._minimum_path_num < 0: self._minimum_path_num = 0 if self._minimum_path_num > self._max_layers: self._minimum_path_num = self._max_layers if self._minimum_path_num > self._current_path_num: self._current_path_num = self._minimum_path_num self._startUpdateTopLayers() self.currentPathNumChanged.emit() ## Set the layer view type # # \param layer_view_type integer as in SimulationView.qml and this class def setSimulationViewType(self, layer_view_type: int) -> None: self._layer_view_type = layer_view_type self.currentLayerNumChanged.emit() ## Return the layer view type, integer as in SimulationView.qml and this class def getSimulationViewType(self) -> int: return self._layer_view_type ## Set the extruder opacity # # \param extruder_nr 0..3 # \param opacity 0.0 .. 1.0 def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None: if 0 <= extruder_nr <= 3: self._extruder_opacity[extruder_nr] = opacity self.currentLayerNumChanged.emit() def getExtruderOpacities(self)-> List[float]: return self._extruder_opacity def setShowTravelMoves(self, show): self._show_travel_moves = show self.currentLayerNumChanged.emit() def getShowTravelMoves(self): return self._show_travel_moves def setShowHelpers(self, show: bool) -> None: self._show_helpers = show self.currentLayerNumChanged.emit() def getShowHelpers(self) -> bool: return self._show_helpers def setShowSkin(self, show: bool) -> None: self._show_skin = show self.currentLayerNumChanged.emit() def getShowSkin(self) -> bool: return self._show_skin def setShowInfill(self, show: bool) -> None: self._show_infill = show self.currentLayerNumChanged.emit() def getShowInfill(self) -> bool: return self._show_infill def getCompatibilityMode(self) -> bool: return self._compatibility_mode def getExtruderCount(self) -> int: return self._extruder_count def getMinFeedrate(self) -> float: if abs(self._min_feedrate - sys.float_info.max) < 10: # Some lenience due to floating point rounding. return 0.0 # If it's still max-float, there are no measurements. Use 0 then. return self._min_feedrate def getMaxFeedrate(self) -> float: return self._max_feedrate def getMinThickness(self) -> float: if abs(self._min_thickness - sys.float_info.max) < 10: # Some lenience due to floating point rounding. return 0.0 # If it's still max-float, there are no measurements. Use 0 then. return self._min_thickness def getMaxThickness(self) -> float: return self._max_thickness def calculateMaxLayers(self) -> None: scene = self.getController().getScene() self._old_max_layers = self._max_layers ## Recalculate num max layers new_max_layers = -1 for node in DepthFirstIterator(scene.getRoot()): # type: ignore layer_data = node.callDecoration("getLayerData") if not layer_data: continue self.setActivity(True) min_layer_number = sys.maxsize max_layer_number = -sys.maxsize for layer_id in layer_data.getLayers(): # If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects if len(layer_data.getLayer(layer_id).polygons) < 1: continue # Store the max and min feedrates and thicknesses for display purposes for p in layer_data.getLayer(layer_id).polygons: self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate) self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate) self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness) try: self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness) except ValueError: # Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding # the zero) can't be calculated Logger.log("i", "Min thickness can't be calculated because all the values are zero") if max_layer_number < layer_id: max_layer_number = layer_id if min_layer_number > layer_id: min_layer_number = layer_id layer_count = max_layer_number - min_layer_number if new_max_layers < layer_count: new_max_layers = layer_count if new_max_layers >= 0 and new_max_layers != self._old_max_layers: self._max_layers = new_max_layers # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first # if it's the largest value. If we don't do this, we can have a slider block outside of the # slider. if new_max_layers > self._current_layer_num: self.maxLayersChanged.emit() self.setLayer(int(self._max_layers)) else: self.setLayer(int(self._max_layers)) self.maxLayersChanged.emit() self._startUpdateTopLayers() def calculateMaxPathsOnLayer(self, layer_num: int) -> None: # Update the currentPath scene = self.getController().getScene() for node in DepthFirstIterator(scene.getRoot()): # type: ignore layer_data = node.callDecoration("getLayerData") if not layer_data: continue layer = layer_data.getLayer(layer_num) if layer is None: return new_max_paths = layer.lineMeshElementCount() if new_max_paths >= 0 and new_max_paths != self._max_paths: self._max_paths = new_max_paths self.maxPathsChanged.emit() self.setPath(int(new_max_paths)) maxLayersChanged = Signal() maxPathsChanged = Signal() currentLayerNumChanged = Signal() currentPathNumChanged = Signal() globalStackChanged = Signal() preferencesChanged = Signal() busyChanged = Signal() activityChanged = Signal() ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): if self._proxy is None: self._proxy = SimulationViewProxy(self) return self._proxy def endRendering(self) -> None: pass def event(self, event) -> bool: modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier if event.type == Event.KeyPressEvent and ctrl_is_active: amount = 10 if shift_is_active else 1 if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + amount) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - amount) return True if event.type == Event.ViewActivateEvent: # Start listening to changes. Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching. # This can happen when you do the following steps: # 1. Start Cura # 2. Load a model # 3. Switch to Custom mode # 4. Select the model and click on the per-object tool icon # 5. Switch view to Layer view or X-Ray # 6. Cura will very likely crash # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why. # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL # context is None. if Platform.isOSX(): if QOpenGLContext.currentContext() is None: Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later") CuraApplication.getInstance().callLater(lambda e=event: self.event(e)) return False # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() self.getRenderer().addRenderPass(layer_pass) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() nozzle.setParent(self.getController().getScene().getRoot()) nozzle.setVisible(False) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() if not self._simulationview_composite_shader: plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("SimulationView")) self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(plugin_path, "simulationview_composite.shader")) theme = CuraApplication.getInstance().getTheme() if theme is not None: self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) if not self._composite_pass: self._composite_pass = cast(CompositePass, self.getRenderer().getRenderPass("composite")) self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader() self._composite_pass.setCompositeShader(self._simulationview_composite_shader) elif event.type == Event.ViewDeactivateEvent: self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) self._wireprint_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) if self._nozzle_node: self._nozzle_node.setParent(None) self.getRenderer().removeRenderPass(self._layer_pass) if self._composite_pass: self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings)) self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader)) return False def getCurrentLayerMesh(self): return self._current_layer_mesh def getCurrentLayerJumps(self): return self._current_layer_jumps def _onGlobalStackChanged(self) -> None: if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack = Application.getInstance().getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") self._onPropertyChanged("wireframe_enabled", "value") self.globalStackChanged.emit() else: self._wireprint_warning_message.hide() def _onPropertyChanged(self, key: str, property_name: str) -> None: if key == "wireframe_enabled" and property_name == "value": if self._global_container_stack and self._global_container_stack.getProperty("wireframe_enabled", "value"): self._wireprint_warning_message.show() else: self._wireprint_warning_message.hide() def _onCurrentLayerNumChanged(self) -> None: self.calculateMaxPathsOnLayer(self._current_layer_num) def _startUpdateTopLayers(self) -> None: if not self._compatibility_mode: return if self._top_layers_job: self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh) self._top_layers_job.cancel() self.setBusy(True) self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) # type: ignore # mypy doesn't understand the whole private class thing that's going on here. self._top_layers_job.start() # type: ignore def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None: self.setBusy(False) if not job.getResult(): return self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data. self._current_layer_mesh = job.getResult().get("layers") if self._show_travel_moves: self._current_layer_jumps = job.getResult().get("jumps") self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot()) self._top_layers_job = None def _updateWithPreferences(self) -> None: self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._compatibility_mode = self._evaluateCompatibilityMode() self.setSimulationViewType(int(float(Application.getInstance().getPreferences().getValue("layerview/layer_view_type")))) for extruder_nr, extruder_opacity in enumerate(Application.getInstance().getPreferences().getValue("layerview/extruder_opacities").split("|")): try: opacity = float(extruder_opacity) except ValueError: opacity = 1.0 self.setExtruderOpacity(extruder_nr, opacity) self.setShowTravelMoves(bool(Application.getInstance().getPreferences().getValue("layerview/show_travel_moves"))) self.setShowHelpers(bool(Application.getInstance().getPreferences().getValue("layerview/show_helpers"))) self.setShowSkin(bool(Application.getInstance().getPreferences().getValue("layerview/show_skin"))) self.setShowInfill(bool(Application.getInstance().getPreferences().getValue("layerview/show_infill"))) self._startUpdateTopLayers() self.preferencesChanged.emit() def _onPreferencesChanged(self, preference: str) -> None: if preference not in { "view/top_layer_count", "view/only_show_top_layers", "view/force_layer_view_compatibility_mode", "layerview/layer_view_type", "layerview/extruder_opacities", "layerview/show_travel_moves", "layerview/show_helpers", "layerview/show_skin", "layerview/show_infill", }: return self._updateWithPreferences()
def exportProfile(self, instance_ids, file_name, file_type): # Parse the fileType to deduce what plugin can save the file format. # fileType has the format "<description> (*.<extension>)" split = file_type.rfind( " (*." ) # Find where the description ends and the extension starts. if split < 0: # Not found. Invalid format. Logger.log("e", "Invalid file format identifier %s", file_type) return description = file_type[:split] extension = file_type[split + 4:-1] # Leave out the " (*." and ")". if not file_name.endswith( "." + extension ): # Auto-fill the extension if the user did not provide any. file_name += "." + extension # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. if not Platform.isWindows(): if os.path.exists(file_name): result = QMessageBox.question( None, catalog.i18nc("@title:window", "File Already Exists"), catalog.i18nc( "@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?" ).format(file_name)) if result == QMessageBox.No: return found_containers = [] extruder_positions = [] for instance_id in instance_ids: containers = ContainerRegistry.getInstance( ).findInstanceContainers(id=instance_id) if containers: found_containers.append(containers[0]) # Determine the position of the extruder of this container extruder_id = containers[0].getMetaDataEntry("extruder", "") if extruder_id == "": # Global stack extruder_positions.append(-1) else: extruder_containers = ContainerRegistry.getInstance( ).findDefinitionContainers(id=extruder_id) if extruder_containers: extruder_positions.append( int(extruder_containers[0].getMetaDataEntry( "position", 0))) else: extruder_positions.append(0) # Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...) found_containers = [ containers for (positions, containers ) in sorted(zip(extruder_positions, found_containers)) ] profile_writer = self._findProfileWriter(extension, description) try: success = profile_writer.write(file_name, found_containers) except Exception as e: Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) m = Message(catalog.i18nc( "@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime=0) m.show() return if not success: Logger.log( "w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) m = Message(catalog.i18nc( "@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime=0) m.show() return m = Message( catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name)) m.show()
def run(self): status_message = Message( i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0, title=i18n_catalog.i18nc("@info:title", "Placing Object")) status_message.show() scene = Application.getInstance().getController().getScene() total_progress = len(self._objects) * self._count current_progress = 0 root = scene.getRoot() arranger = Arrange.create(scene_root=root) nodes = [] for node in self._objects: # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent( ).callDecoration("isGroup"): current_node = current_node.getParent() node_too_big = False if node.getBoundingBox().width < 300 or node.getBoundingBox( ).depth < 300: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode( current_node, min_offset=self._min_offset) else: node_too_big = True found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: new_node, solution_found = arranger.findNodePlacement( current_node, offset_shape_arr, hull_shape_arr) if node_too_big or not solution_found: found_solution_for_all = False new_location = new_node.getPosition() new_location = new_location.set(z=100 - i * 20) new_node.setPosition(new_location) # Same build plate build_plate_number = current_node.callDecoration( "getBuildPlateNumber") new_node.callDecoration("setBuildPlateNumber", build_plate_number) for child in new_node.getChildren(): child.callDecoration("setBuildPlateNumber", build_plate_number) nodes.append(new_node) current_progress += 1 status_message.setProgress( (current_progress / total_progress) * 100) Job.yieldThread() Job.yieldThread() if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation( AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc( "@info:status", "Unable to find a location within the build volume for all objects" ), title=i18n_catalog.i18nc( "@info:title", "Placing Object")) no_full_solution_message.show()
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: sync_message.hide() self.discrepancies.emit(self._model)
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: 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" if Platform.isWindows(): executable_name += ".exe" default_engine_location = executable_name if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)): default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name) if hasattr(sys, "frozen"): default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name) if Platform.isLinux() and not default_engine_location: if not os.getenv("PATH"): raise OSError("There is something wrong with your Linux installation.") for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep): execpath = os.path.join(pathdir, executable_name) if os.path.exists(execpath): default_engine_location = execpath break self._application = CuraApplication.getInstance() #type: CuraApplication self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._machine_error_checker = None #type: Optional[MachineErrorChecker] if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) self._application.getPreferences().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False #type: bool 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._scene = self._application.getController().getScene() #type: Scene self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: # - auto-slicing is started with a timer # - 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, 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] # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage self._message_handlers["cura.proto.Progress"] = self._onProgressMessage self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage 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._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._slice_start_time = None #type: Optional[float] self._is_disabled = False #type: bool self._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. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. self._change_timer = QTimer() #type: QTimer self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.initializationFinished.connect(self.initialize) def initialize(self) -> None: self._multi_build_plate_model = self._application.getMultiBuildPlateModel() self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) if self._multi_build_plate_model: 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. # Called when closing the application. def close(self) -> None: # 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]: json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") return [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] ## 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. slicingStarted = Signal() ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() @pyqtSlot() def stopSlicing(self) -> None: self.backendStateChange.emit(BackendState.NotStarted) if self._slicing: # We were already slicing. Stop the old job. 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. Logger.log("d", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None if self._error_message: self._error_message.hide() ## Manually triggers a reslice @pyqtSlot() def forceSlice(self) -> None: self.markSliceAll() self.slice() ## Perform a slice of the scene. def slice(self) -> None: Logger.log("d", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") return if self._process_layers_job: Logger.log("d", "Process layers job still busy, trying later.") return if not hasattr(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 build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjectsPerBuildPlate() self._stored_layer_data = [] 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] = [] #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() return if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) if self._process is None: # type: ignore self._createSocket() self.stopSlicing() self._engine_is_fresh = False # Yes we're going to use the engine 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._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(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() self._start_slice_job.finished.connect(self._onStartSliceCompleted) ## Terminate the engine process. # Start the engine process by calling _createSocket() def _terminate(self) -> None: self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: del self._stored_optimized_layer_data[self._start_slice_job_build_plate] if self._start_slice_job is not None: self._start_slice_job.cancel() self.slicingCancelled.emit() self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") if self._application.getUseExternalBackend(): return 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 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: if self._error_message: self._error_message.hide() # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error: self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) return if job.getResult() == StartJobResult.SettingError: if self._application.platformActivity: if not self._global_container_stack: Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) error_keys = [] #type: 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. 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. Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key)) continue error_labels.add(definitions[0].label) 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) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) return elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} 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(): if not self._global_container_stack: Logger.log("e", "CuraEngineBackend does not have global_container_stack assigned.") continue 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 errors[key] = definition[0].label 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() == 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) 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() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return 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")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) else: self.backendStateChange.emit(BackendState.NotStarted) self._invokeSlice() return # 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 self.backendStateChange.emit(BackendState.Processing) 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: enable_timer = True self._is_disabled = False if not self._application.getPreferences().getValue("general/auto_slice"): enable_timer = False 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 #type: ignore #Because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer if enable_timer: self.backendStateChange.emit(BackendState.NotStarted) self.enableTimer() return True else: self.disableTimer() return False ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: 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") if build_plate_number is not None: 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: if not isinstance(source, SceneNode): return # This case checks if the source node is a node that contains GCode. In this case the # current layer data is removed so the previous data is not rendered - CURA-4821 if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"): self._stored_optimized_layer_data = {} build_plate_changed = set() source_build_plate_number = source.callDecoration("getBuildPlateNumber") if source == self._scene.getRoot(): # we got the root node num_objects = self._numObjectsPerBuildPlate() for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()): if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]: self._last_num_objects[build_plate_number] = num_objects[build_plate_number] build_plate_changed.add(build_plate_number) else: # we got a single scenenode if not source.callDecoration("isGroup"): mesh_data = source.getMeshData() if mesh_data is None or mesh_data.getVertices() is None: return # There are some SceneNodes that do not have any build plate associated, then do not add to the list. if source_build_plate_number is not None: build_plate_changed.add(source_build_plate_number) if not build_plate_changed: return if self._tool_active: # do it later, each source only has to be done once if source not in self._postponed_scene_change_sources: self._postponed_scene_change_sources.append(source) return self.stopSlicing() for build_plate_number in build_plate_changed: if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) self.printDurationMessage.emit(source_build_plate_number, {}, []) self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) # if not self._use_timer: # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData(build_plate_changed) 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: if self._application.isShuttingDown(): return super()._onSocketError(error) if error.getErrorCode() == Arcus.ErrorCode.Debug: return self._terminate() self._createSocket() 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" if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: self._start_slice_job.setIsCancelled(False) ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: # Clear out any old gcode self._scene.gcode_dict = {} # type: ignore 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) def markSliceAll(self) -> None: for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1): 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: self.stopSlicing() self.markSliceAll() self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) if not self._use_timer: # 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: if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() elif property == "validationState": if self._use_timer: self._change_timer.stop() def _onStackErrorCheckFinished(self) -> None: self.determineAutoSlicing() if self._is_disabled: return if not self._slicing and self._build_plates_to_be_sliced: 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: 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: 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: self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) def _invokeSlice(self) -> None: 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._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() ## 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: self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) 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)) replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights)) replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts)) replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName)) gcode_list[index] = replaced self._slicing = False if self._slice_start_time: Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate())) # See if we need to process the sliced layers job. active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate if ( self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate and active_build_plate not in self._build_plates_to_be_sliced): self._startProcessSlicedLayersJob(active_build_plate) # self._onActiveViewChanged() self._start_slice_job_build_plate = None Logger.log("d", "See if there is more to slice...") # Somehow this results in an Arcus Error # self.slice() # Call slice again using the timer, allowing the backend to restart if self._build_plates_to_be_sliced: 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: 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")) #type: ignore #Because we generate this attribute dynamically. ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: if not protocol_file: plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if not plugin_path: Logger.log("e", "Could not get plugin path!", self.getPluginId()) return protocol_file = os.path.abspath(os.path.join(plugin_path, "Cura.proto")) 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: self.needsSlicing() 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._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() ## 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: material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) 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]: result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, "skin": message.time_skin, "infill": message.time_infill, "support_infill": message.time_support_infill, "support_interface": message.time_support_interface, "support": message.time_support, "skirt": message.time_skirt, "travel": message.time_travel, "retract": message.time_retract, "none": message.time_none } return result ## Called when the back-end connects to the front-end. def _onBackendConnected(self) -> None: 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: 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 if not self._engine_is_fresh: 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: self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes while self._postponed_scene_change_sources: source = self._postponed_scene_change_sources.pop(0) self._onSceneChanged(source) def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None: 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() ## Called when the user changes the active view mode. def _onActiveViewChanged(self) -> None: view = self._application.getController().getActiveView() if view: active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate 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. # TODO: what build plate I am slicing if (active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job and active_build_plate not in self._build_plates_to_be_sliced): self._startProcessSlicedLayersJob(active_build_plate) 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: 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: if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.containersChanged.disconnect(self._onChanged) self._global_container_stack = self._application.getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. self._global_container_stack.containersChanged.connect(self._onChanged) extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.connect(self._onSettingChanged) extruder.containersChanged.connect(self._onChanged) self._onChanged() def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None: del self._stored_optimized_layer_data[job.getBuildPlate()] self._process_layers_job = None Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() ## Connect slice function to timer. def enableTimer(self) -> None: 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: if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) def _onPreferencesChanged(self, preference: str) -> None: if preference != "general/auto_slice": return auto_slice = self.determineAutoSlicing() if auto_slice: self._change_timer.start() ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self) -> None: if self._use_timer: self._change_timer.start() def _extruderChanged(self) -> None: if not self._multi_build_plate_model: Logger.log("w", "CuraEngineBackend does not have multi_build_plate_model assigned!") return 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()
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): """Request the specified nodes to be written to the removable drive. :param nodes: A collection of scene nodes that should be written to the removable drive. :param file_name: :type{string} A suggestion for the file name to write to. If none is provided, a file name will be made from the names of the meshes. :param limit_mimetypes: Should we limit the available MIME types to the MIME types available to the currently active machine? """ filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() # Formats supported by this application (File types that we can actually write) if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: file_formats = Application.getInstance().getMeshFileHandler( ).getSupportedFileTypesWrite() if filter_by_machine: container = Application.getInstance().getGlobalContainerStack( ).findContainer({"file_formats": "*"}) # Create a list from supported file formats string machine_file_formats = [ file_type.strip() for file_type in container.getMetaDataEntry( "file_formats").split(";") ] # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = { format["mime_type"]: format for format in file_formats } file_formats = [ format_by_mimetype[mimetype] for mimetype in machine_file_formats if mimetype in format_by_mimetype ] # Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc( "@info:status", "There are no file formats available to write with!")) preferred_format = file_formats[0] # Just take the first file format available. if file_handler is not None: writer = file_handler.getWriterByMimeType( preferred_format["mime_type"]) else: writer = Application.getInstance().getMeshFileHandler( ).getWriterByMimeType(preferred_format["mime_type"]) extension = preferred_format["extension"] if file_name is None: file_name = self._automaticFileName(nodes) if extension: # Not empty string. extension = "." + extension file_name = os.path.join(self.getId(), file_name + extension) try: Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode if preferred_format["mode"] == FileWriter.OutputMode.TextMode: self._stream = open(file_name, "wt", buffering=1, encoding="utf-8") else: #Binary mode. self._stream = open(file_name, "wb", buffering=1) job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"]) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) message = Message( catalog.i18nc( "@info:progress Don't translate the XML tags <filename>!", "Saving to Removable Drive <filename>{0}</filename>"). format(self.getName()), 0, False, -1, catalog.i18nc("@info:title", "Saving")) message.show() self.writeStarted.emit(self) job.setMessage(message) self._writing = True job.start() except PermissionError as e: Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e)) raise OutputDeviceError.PermissionDeniedError( catalog.i18nc( "@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>" ).format(file_name, str(e))) from e except OSError as e: Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError( catalog.i18nc( "@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>" ).format(file_name, str(e))) from e
def _onFinished(self, job): if self._stream: # Explicitly closing the stream flushes the write-buffer try: self._stream.close() self._stream = None except: Logger.logException( "w", "An execption occured while trying to write to removable drive." ) message = Message(catalog.i18nc( "@info:status", "Could not save to removable drive {0}: {1}").format( self.getName(), str(job.getError())), title=catalog.i18nc("@info:title", "Error")) message.show() self.writeError.emit(self) return self._writing = False self.writeFinished.emit(self) if job.getResult(): message = Message(catalog.i18nc( "@info:status", "Saved to Removable Drive {0} as {1}").format( self.getName(), os.path.basename(job.getFileName())), title=catalog.i18nc("@info:title", "File Saved")) message.addAction( "eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format( self.getName())) message.actionTriggered.connect(self._onActionTriggered) message.show() self.writeSuccess.emit(self) else: message = Message(catalog.i18nc( "@info:status", "Could not save to removable drive {0}: {1}").format( self.getName(), str(job.getError())), title=catalog.i18nc("@info:title", "Warning")) message.show() self.writeError.emit(self) job.getStream().close()