def load_file(self, file_name, mesh_builder, _use_numpystl = False): file_read = False if _use_numpystl: Logger.log("i", "Using NumPy-STL to load STL data.") try: self._loadWithNumpySTL(file_name, mesh_builder) file_read = True except: Logger.logException("e", "Reading file failed with Numpy-STL!") if not file_read: Logger.log("i", "Using legacy code to load STL data.") f = open(file_name, "rb") if not self._loadBinary(mesh_builder, f): f.close() f = open(file_name, "rt", encoding = "utf-8") try: self._loadAscii(mesh_builder, f) except UnicodeDecodeError: return None f.close() Job.yieldThread() # Yield somewhat to ensure the GUI has time to update a bit. mesh_builder.calculateNormals(fast = True) mesh_builder.setFileName(file_name)
def _buildGlobalSettingsMessage(self, stack): keys = stack.getAllKeys() settings = {} for key in keys: # Use resolvement value if available, or take the value resolved_value = stack.getProperty(key, "resolve") if resolved_value is not None: # There is a resolvement value. Check if we need to use it. user_container = stack.findContainer({"type": "user"}) quality_changes_container = stack.findContainer({"type": "quality_changes"}) if user_container.hasProperty(key,"value") or quality_changes_container.hasProperty(key,"value"): # Normal case settings[key] = stack.getProperty(key, "value") else: settings[key] = resolved_value else: # Normal case settings[key] = stack.getProperty(key, "value") Job.yieldThread() start_gcode = settings["machine_start_gcode"] settings["material_bed_temp_prepend"] = "{material_bed_temperature}" not in start_gcode #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend settings["material_print_temp_prepend"] = "{material_print_temperature}" not in start_gcode for key, value in settings.items(): #Add all submessages for each individual setting. setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings") setting_message.name = key if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting. setting_message.value = self._expandGcodeTokens(key, value, settings) else: setting_message.value = str(value).encode("utf-8") Job.yieldThread()
def _loadAscii(self, mesh_builder, f): num_verts = 0 for lines in f: for line in lines.split("\r"): if "vertex" in line: num_verts += 1 mesh_builder.reserveFaceCount(num_verts / 3) f.seek(0, os.SEEK_SET) vertex = 0 face = [None, None, None] for lines in f: for line in lines.split("\r"): if "vertex" in line: face[vertex] = line.split()[1:] vertex += 1 if vertex == 3: mesh_builder.addFaceByPoints( float(face[0][0]), float(face[0][2]), -float(face[0][1]), float(face[1][0]), float(face[1][2]), -float(face[1][1]), float(face[2][0]), float(face[2][2]), -float(face[2][1]) ) vertex = 0 Job.yieldThread()
def _buildGlobalSettingsMessage(self, stack): keys = stack.getAllKeys() settings = {} for key in keys: settings[key] = stack.getProperty(key, "value") Job.yieldThread() start_gcode = settings["machine_start_gcode"] #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings)) print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"} settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings)) settings["print_bed_temperature"] = settings["material_bed_temperature"] settings["print_temperature"] = settings["material_print_temperature"] settings["time"] = time.strftime('%H:%M:%S') settings["date"] = time.strftime('%d-%m-%Y') settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))] for key, value in settings.items(): #Add all submessages for each individual setting. setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings") setting_message.name = key if key == "machine_start_gcode" or key == "machine_end_gcode" or key == "machine_extruder_start_code" or key == "machine_extruder_end_code": #If it's a g-code message, use special formatting. setting_message.value = self._expandGcodeTokens(key, value, settings) else: setting_message.value = str(value).encode("utf-8") Job.yieldThread()
def _buildExtruderMessage(self, stack: ContainerStack) -> None: message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: self._cacheAllExtruderSettings() if self._all_extruders_settings is None: return extruder_nr = stack.getProperty("extruder_nr", "value") settings = self._all_extruders_settings[str(extruder_nr)].copy() # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") # Replace the setting tokens in start and end g-code. extruder_nr = stack.getProperty("extruder_nr", "value") settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr) settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr) for key, value in settings.items(): # Do not send settings that are not settable_per_extruder. if not stack.getProperty(key, "settable_per_extruder"): continue setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key setting.value = str(value).encode("utf-8") Job.yieldThread()
def read(self, file_name): mesh_builder = MeshBuilder() scene_node = SceneNode() if use_numpystl: self._loadWithNumpySTL(file_name, mesh_builder) else: f = open(file_name, "rb") if not self._loadBinary(mesh_builder, f): f.close() f = open(file_name, "rt") try: self._loadAscii(mesh_builder, f) except UnicodeDecodeError: return None f.close() Job.yieldThread() # Yield somewhat to ensure the GUI has time to update a bit. mesh_builder.calculateNormals(fast = True) mesh = mesh_builder.build() Logger.log("d", "Loaded a mesh with %s vertices", mesh_builder.getVertexCount()) scene_node.setMeshData(mesh) return scene_node
def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage): stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. if not stack: return # Check all settings for relations, so we can also calculate the correct values for dependent settings. top_of_stack = stack.getTop() # Cache for efficiency. changed_setting_keys = top_of_stack.getAllKeys() # Add all relations to changed settings as well. for key in top_of_stack.getAllKeys(): instance = top_of_stack.getInstance(key) self._addRelations(changed_setting_keys, instance.definition.relations) Job.yieldThread() # Ensure that the engine is aware what the build extruder is. changed_setting_keys.add("extruder_nr") # Get values for all changed settings for key in changed_setting_keys: setting = message.addRepeatedMessage("settings") setting.name = key extruder = int(round(float(stack.getProperty(key, "limit_to_extruder")))) # Check if limited to a specific extruder, but not overridden by per-object settings. if extruder >= 0 and key not in changed_setting_keys: limited_stack = ExtruderManager.getInstance().getActiveExtruderStacks()[extruder] else: limited_stack = stack setting.value = str(limited_stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def _buildGlobalSettingsMessage(self, stack): settings = self._buildReplacementTokens(stack) # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend start_gcode = settings["machine_start_gcode"] bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"] pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr} settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"] pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr} settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None # Replace the setting tokens in start and end g-code. # Use values from the first used extruder by default so we get the expected temperatures initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr) settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr) # Add all sub-messages for each individual setting. for key, value in settings.items(): setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings") setting_message.name = key setting_message.value = str(value).encode("utf-8") Job.yieldThread()
def _buildGlobalInheritsStackMessage(self, stack): for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key setting_extruder.extruder = extruder_position Job.yieldThread()
def _buildExtruderMessage(self, stack): message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) for key in stack.getAllKeys(): setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def _handlePerObjectSettings(self, node, message): stack = node.callDecoration("getStack") if stack: for key in stack.getAllKeys(): setting = message.addRepeatedMessage("settings") setting.name = key setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def run(self): loading_message = Message(i18n_catalog.i18nc("@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime = 0, dismissable = False) loading_message.setProgress(-1) loading_message.show() Job.yieldThread() # Yield to any other thread that might want to do something else. try: begin_time = time.time() node = self._handler.read(self._filename) end_time = time.time() Logger.log("d", "Loading mesh took %s seconds", end_time - begin_time) except Exception as e: print(e) if not node: loading_message.hide() result_message = Message(i18n_catalog.i18nc("@info:status", "Failed to load <filename>{0}</filename>", self._filename)) result_message.show() return if node.getMeshData(): node.getMeshData().setFileName(self._filename) # 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 node._resetAABB() bounding_box = node.getBoundingBox() timeout_counter = 0 #As the calculation of the bounding box is in a seperate thread it might be that it's not done yet. while bounding_box.width == 0 or bounding_box.height == 0 or bounding_box.depth == 0: bounding_box = node.getBoundingBox() time.sleep(0.1) timeout_counter += 1 if timeout_counter > 10: break if max_bounds.width < bounding_box.width or max_bounds.height < bounding_box.height or max_bounds.depth < bounding_box.depth: scale_factor_width = max_bounds.width / bounding_box.width scale_factor_height = max_bounds.height / bounding_box.height scale_factor_depth = max_bounds.depth / bounding_box.depth scale_factor = min(scale_factor_width,scale_factor_height,scale_factor_depth) scale_vector = Vector(scale_factor, scale_factor, scale_factor) display_scale_factor = scale_factor * 100 if Preferences.getInstance().getValue("mesh/scale_to_fit") == True: scale_message = Message(i18n_catalog.i18nc("@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor))) try: node.scale(scale_vector) scale_message.show() except Exception as e: print(e) self.setResult(node) loading_message.hide() result_message = Message(i18n_catalog.i18nc("@info:status", "Loaded <filename>{0}</filename>", self._filename)) result_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 run(self): if not self._node: return ## If the scene node is a group, use the hull of the children to calculate its hull. if self._node.callDecoration("isGroup"): hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) for child in self._node.getChildren(): child_hull = child.callDecoration("getConvexHull") if child_hull: hull.setPoints(numpy.append(hull.getPoints(), child_hull.getPoints(), axis = 0)) if hull.getPoints().size < 3: self._node.callDecoration("setConvexHull", None) self._node.callDecoration("setConvexHullJob", None) return Job.yieldThread() else: if not self._node.getMeshData(): return mesh = self._node.getMeshData() vertex_data = mesh.getTransformed(self._node.getWorldTransformation()).getVertices() # Don't use data below 0. TODO; We need a better check for this as this gives poor results for meshes with long edges. vertex_data = vertex_data[vertex_data[:,1]>0] hull = Polygon(numpy.rint(vertex_data[:, [0, 2]]).astype(int)) # First, calculate the normal convex hull around the points hull = hull.getConvexHull() # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. # This is done because of rounding errors. hull = hull.getMinkowskiHull(Polygon(numpy.array([[-1, -1], [-1, 1], [1, 1], [1, -1]], numpy.float32))) profile = Application.getInstance().getMachineManager().getActiveProfile() if profile: if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): # Printing one at a time and it's not an object in a group self._node.callDecoration("setConvexHullBoundary", copy.deepcopy(hull)) head_hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_with_fans_polygon"),numpy.float32))) self._node.callDecoration("setConvexHullHead", head_hull) hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_polygon"),numpy.float32))) else: self._node.callDecoration("setConvexHullHead", None) hull_node = ConvexHullNode.ConvexHullNode(self._node, hull, Application.getInstance().getController().getScene().getRoot()) self._node.callDecoration("setConvexHullNode", hull_node) self._node.callDecoration("setConvexHull", hull) self._node.callDecoration("setConvexHullJob", None) if self._node.getParent().callDecoration("isGroup"): job = self._node.getParent().callDecoration("getConvexHullJob") if job: job.cancel() self._node.getParent().callDecoration("setConvexHull", None) hull_node = self._node.getParent().callDecoration("getConvexHullNode") if hull_node: hull_node.setParent(None)
def _handlePerObjectSettings(self, node, message): stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. if stack and stack.getTop().getAllKeys(): # Because we want to use inheritance correctly, we send all settings as seen from the per object stack. for key in stack.getAllKeys(): setting = message.addRepeatedMessage("settings") setting.name = key setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def _buildExtruderMessageFromGlobalStack(self, stack): message = self._slice_message.addRepeatedMessage("extruders") for key in stack.getAllKeys(): # Do not send settings that are not settable_per_extruder. if not stack.getProperty(key, "settable_per_extruder"): continue setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def _checkStackForErrors(self, stack): if stack is None: return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
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 _buildReplacementTokens(self, stack) -> dict: result = {} for key in stack.getAllKeys(): result[key] = stack.getProperty(key, "value") Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] #Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] return result
def _buildExtruderMessage(self, stack): message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) material_instance_container = stack.findContainer({"type": "material"}) for key in stack.getAllKeys(): setting = message.getMessage("settings").addRepeatedMessage("settings") setting.name = key if key == "material_guid" and material_instance_container: # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8") else: setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
def run(self): layer_data = None for node in DepthFirstIterator(self._scene.getRoot()): layer_data = node.callDecoration("getLayerData") if layer_data: break if self._cancel or not layer_data: return layer_mesh = MeshBuilder() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: continue try: layer = layer_data.getLayer(layer_number).createMesh() except Exception: Logger.logException("w", "An exception occurred while creating layer mesh.") return if not layer or layer.getVertices() is None: continue layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices()) layer_mesh.addVertices(layer.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0 brightness[0, 3] = 1.0 layer_mesh.addColors(layer.getColors() * brightness) if self._cancel: return Job.yieldThread() if self._cancel: return Job.yieldThread() jump_mesh = layer_data.getLayer(self._layer_number).createJumps() if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
def run(self) -> None: if self._handler is None: Logger.log("e", "FileHandler was not set.") return None reader = self._handler.getReaderForFile(self._filename) if not reader: result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Cannot open files of the type of <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File")) result_message.show() return # Give the plugin a chance to display a dialog before showing the loading UI try: pre_read_result = reader.preRead(self._filename) except: Logger.logException("e", "Failed to pre-read the file %s", self._filename) pre_read_result = MeshReader.PreReadResult.failed if pre_read_result != MeshReader.PreReadResult.accepted: if pre_read_result == MeshReader.PreReadResult.failed: result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File")) result_message.show() return self._loading_message = Message(self._filename, lifetime=0, dismissable=False, title = i18n_catalog.i18nc("@info:title", "Loading")) self._loading_message.setProgress(-1) self._loading_message.show() Job.yieldThread() # Yield to any other thread that might want to do something else. begin_time = time.time() try: self.setResult(self._handler.readerRead(reader, self._filename)) except: Logger.logException("e", "Exception occurred while loading file %s", self._filename) finally: end_time = time.time() Logger.log("d", "Loading file took %0.1f seconds", end_time - begin_time) if self._result is None: self._loading_message.hide() result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File")) result_message.show() return self._loading_message.hide()
def run(self): layer_data = None for node in DepthFirstIterator(self._scene.getRoot()): layer_data = node.callDecoration("getLayerData") if layer_data: break if self._cancel or not layer_data: return layer_mesh = MeshData() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: continue try: layer = layer_data.getLayer(layer_number).createMesh() except Exception as e: print(e) return if not layer or layer.getVertices() is None: continue layer_mesh.addVertices(layer.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 layer_mesh.addColors(layer.getColors() * brightness) if self._cancel: return Job.yieldThread() if self._cancel: return Job.yieldThread() jump_mesh = layer_data.getLayer(self._layer_number).createJumps() if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None self.setResult({ "layers": layer_mesh, "jumps": jump_mesh })
def load_file(self, file_name, mesh_builder, _use_numpystl = False): if _use_numpystl: self._loadWithNumpySTL(file_name, mesh_builder) else: f = open(file_name, "rb") if not self._loadBinary(mesh_builder, f): f.close() f = open(file_name, "rt") try: self._loadAscii(mesh_builder, f) except UnicodeDecodeError: return None f.close() Job.yieldThread() # Yield somewhat to ensure the GUI has time to update a bit. mesh_builder.calculateNormals(fast = True) mesh_builder.setFileName(file_name)
def run(self): Job.yieldThread() if self._writer.write(self._stream, self._node, self._mode): self._stream.seek(0) select_bool = str(Preferences.getInstance().getValue("octoprint/select")).lower() print_bool = str(Preferences.getInstance().getValue("octoprint/print")).lower() url = Preferences.getInstance().getValue("octoprint/base_url") + "api/files/local" api_key = Preferences.getInstance().getValue("octoprint/api_key") result = requests.post(url, files={'file': (self._file_name, self._stream), 'select': ('', select_bool), 'print': ('', print_bool)}, headers={'User-agent': 'Cura AutoUploader Plugin', 'X-Api-Key': api_key}) self.setResult(result) else: self.setResult(False)
def _buildReplacementTokens(self, stack) -> dict: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
def _buildGlobalSettingsMessage(self, stack): settings = self._buildReplacementTokens(stack) start_gcode = settings["machine_start_gcode"] #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings)) print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"} settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings)) #Replace the setting tokens in start and end g-code. settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], settings) settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], settings) for key, value in settings.items(): #Add all submessages for each individual setting. setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings") setting_message.name = key setting_message.value = str(value).encode("utf-8") Job.yieldThread()
def _handlePerObjectSettings(self, node, message): profile = node.callDecoration("getProfile") if profile: for key, value in profile.getAllSettingValues().items(): setting = message.addRepeatedMessage("settings") setting.name = key setting.value = str(value).encode() Job.yieldThread() object_settings = node.callDecoration("getAllSettingValues") if not object_settings: return for key, value in object_settings.items(): setting = message.addRepeatedMessage("settings") setting.name = key setting.value = str(value).encode() Job.yieldThread()
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) offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) 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. node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) if 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()
def _handlePerObjectSettings(self, node, message): stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. if stack: # Check all settings for relations, so we can also calculate the correct values for dependant settings. changed_setting_keys = set(stack.getTop().getAllKeys()) for key in stack.getTop().getAllKeys(): instance = stack.getTop().getInstance(key) self._addRelations(changed_setting_keys, instance.definition.relations) Job.yieldThread() # Ensure that the engine is aware what the build extruder is if stack.getProperty("machine_extruder_count", "value") > 1: changed_setting_keys.add("extruder_nr") # Get values for all changed settings for key in changed_setting_keys: setting = message.addRepeatedMessage("settings") setting.name = key setting.value = str(stack.getProperty(key, "value")).encode("utf-8") Job.yieldThread()
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) # Same build plate build_plate_number = current_node.callDecoration( "getBuildPlateNumber") node.callDecoration("setBuildPlateNumber", build_plate_number) 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 processChildNodes(self, node): for c in node: self.processNode(c) Job.yieldThread()
def run(self): if not self._node: return ## If the scene node is a group, use the hull of the children to calculate its hull. if self._node.callDecoration("isGroup"): hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) for child in self._node.getChildren(): child_hull = child.callDecoration("getConvexHull") if child_hull: hull.setPoints( numpy.append(hull.getPoints(), child_hull.getPoints(), axis=0)) if hull.getPoints().size < 3: self._node.callDecoration("setConvexHull", None) self._node.callDecoration("setConvexHullJob", None) return Job.yieldThread() else: if not self._node.getMeshData(): return mesh = self._node.getMeshData() vertex_data = mesh.getTransformed( self._node.getWorldTransformation()).getVertices() # Don't use data below 0. TODO; We need a better check for this as this gives poor results for meshes with long edges. vertex_data = vertex_data[vertex_data[:, 1] >= 0] # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices # This is done to greatly speed up further convex hull calculations as the convex hull # becomes much less complex when dealing with highly detailed models. vertex_data = numpy.round(vertex_data, 1) vertex_data = vertex_data[:, [ 0, 2 ]] # Drop the Y components to project to 2D. # Grab the set of unique points. # # This basically finds the unique rows in the array by treating them as opaque groups of bytes # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( numpy.dtype( (numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) _, idx = numpy.unique(vertex_byte_view, return_index=True) vertex_data = vertex_data[idx] # Select the unique rows by index. hull = Polygon(vertex_data) # First, calculate the normal convex hull around the points hull = hull.getConvexHull() # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. # This is done because of rounding errors. hull = hull.getMinkowskiHull( Polygon( numpy.array( [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) profile = Application.getInstance().getMachineManager( ).getWorkingProfile() if profile: if profile.getSettingValue( "print_sequence" ) == "one_at_a_time" and not self._node.getParent().callDecoration( "isGroup"): # Printing one at a time and it's not an object in a group self._node.callDecoration("setConvexHullBoundary", copy.deepcopy(hull)) head_and_fans = Polygon( numpy.array( profile.getSettingValue( "machine_head_with_fans_polygon"), numpy.float32)) # Full head hull is used to actually check the order. full_head_hull = hull.getMinkowskiHull(head_and_fans) self._node.callDecoration("setConvexHullHeadFull", full_head_hull) mirrored = copy.deepcopy(head_and_fans) mirrored.mirror([0, 0], [0, 1]) #Mirror horizontally. mirrored.mirror([0, 0], [1, 0]) #Mirror vertically. head_and_fans = head_and_fans.intersectionConvexHulls(mirrored) # Add extra margin depending on adhesion type adhesion_type = profile.getSettingValue("adhesion_type") extra_margin = 0 machine_head_coords = numpy.array( profile.getSettingValue("machine_head_with_fans_polygon"), numpy.float32) head_y_size = abs(machine_head_coords).min( ) # safe margin to take off in all directions if adhesion_type == "raft": extra_margin = max( 0, profile.getSettingValue("raft_margin") - head_y_size) elif adhesion_type == "brim": extra_margin = max( 0, profile.getSettingValue("brim_width") - head_y_size) elif adhesion_type == "skirt": extra_margin = max( 0, profile.getSettingValue("skirt_gap") + profile.getSettingValue("skirt_line_count") * profile.getSettingValue("skirt_line_width") - head_y_size) # adjust head_and_fans with extra margin if extra_margin > 0: # In Cura 2.2+, there is a function to create this circle-like polygon. extra_margin_polygon = Polygon( numpy.array( [[-extra_margin, 0], [-extra_margin * 0.707, extra_margin * 0.707], [0, extra_margin], [extra_margin * 0.707, extra_margin * 0.707], [extra_margin, 0], [extra_margin * 0.707, -extra_margin * 0.707], [0, -extra_margin], [-extra_margin * 0.707, -extra_margin * 0.707]], numpy.float32)) head_and_fans = head_and_fans.getMinkowskiHull( extra_margin_polygon) # Min head hull is used for the push free min_head_hull = hull.getMinkowskiHull(head_and_fans) self._node.callDecoration("setConvexHullHead", min_head_hull) hull = hull.getMinkowskiHull( Polygon( numpy.array( profile.getSettingValue("machine_head_polygon"), numpy.float32))) else: self._node.callDecoration("setConvexHullHead", None) if self._node.getParent( ) is None: #Node was already deleted before job is done. self._node.callDecoration("setConvexHullNode", None) self._node.callDecoration("setConvexHull", None) self._node.callDecoration("setConvexHullJob", None) return hull_node = ConvexHullNode.ConvexHullNode( self._node, hull, Application.getInstance().getController().getScene().getRoot()) self._node.callDecoration("setConvexHullNode", hull_node) self._node.callDecoration("setConvexHull", hull) self._node.callDecoration("setConvexHullJob", None) if self._node.getParent() and self._node.getParent().callDecoration( "isGroup"): job = self._node.getParent().callDecoration("getConvexHullJob") if job: job.cancel() self._node.getParent().callDecoration("setConvexHull", None) hull_node = self._node.getParent().callDecoration( "getConvexHullNode") if hull_node: hull_node.setParent(None)
def _generateSceneNode(self, file_name, xz_size, height_from_base, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm): scene_node = SceneNode() mesh = MeshBuilder() img = QImage(file_name) if img.isNull(): Logger.log("e", "Image is corrupt.") return None width = max(img.width(), 2) height = max(img.height(), 2) aspect = height / width if img.width() < 2 or img.height() < 2: img = img.scaled(width, height, Qt.IgnoreAspectRatio) height_from_base = max(height_from_base, 0) base_height = max(base_height, 0) peak_height = base_height + height_from_base xz_size = max(xz_size, 1) scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: scale_vector = scale_vector.set(z=scale_vector.z * aspect) elif height > width: scale_vector = scale_vector.set(x=scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width if height > width: scale_factor = max_size / height width = int(max(round(width * scale_factor), 2)) height = int(max(round(height * scale_factor), 2)) img = img.scaled(width, height, Qt.IgnoreAspectRatio) width_minus_one = width - 1 height_minus_one = height - 1 Job.yieldThread() texel_width = 1.0 / (width_minus_one) * scale_vector.x texel_height = 1.0 / (height_minus_one) * scale_vector.z height_data = numpy.zeros((height, width), dtype=numpy.float32) for x in range(0, width): for y in range(0, height): qrgb = img.pixel(x, y) if use_transparency_model: height_data[y, x] = ( 0.299 * math.pow(qRed(qrgb) / 255.0, 2.2) + 0.587 * math.pow(qGreen(qrgb) / 255.0, 2.2) + 0.114 * math.pow(qBlue(qrgb) / 255.0, 2.2)) else: height_data[y, x] = ( 0.212655 * qRed(qrgb) + 0.715158 * qGreen(qrgb) + 0.072187 * qBlue(qrgb) ) / 255 # fast computation ignoring gamma and degamma Job.yieldThread() if lighter_is_higher == use_transparency_model: height_data = 1 - height_data for _ in range(0, blur_iterations): copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode="edge") height_data += copy[1:-1, 2:] height_data += copy[1:-1, :-2] height_data += copy[2:, 1:-1] height_data += copy[:-2, 1:-1] height_data += copy[2:, 2:] height_data += copy[:-2, 2:] height_data += copy[2:, :-2] height_data += copy[:-2, :-2] height_data /= 9 Job.yieldThread() if use_transparency_model: divisor = 1.0 / math.log( transmittance_1mm / 100.0 ) # log-base doesn't matter here. Precompute this value for faster computation of each pixel. min_luminance = (transmittance_1mm / 100.0)**(peak_height - base_height) for (y, x) in numpy.ndindex(height_data.shape): mapped_luminance = min_luminance + ( 1.0 - min_luminance) * height_data[y, x] height_data[y, x] = base_height + divisor * math.log( mapped_luminance ) # use same base as a couple lines above this else: height_data *= scale_vector.y height_data += base_height if img.hasAlphaChannel(): for x in range(0, width): for y in range(0, height): height_data[y, x] *= qAlpha(img.pixel(x, y)) / 255.0 heightmap_face_count = 2 * height_minus_one * width_minus_one total_face_count = heightmap_face_count + (width_minus_one * 2) * ( height_minus_one * 2) + 2 mesh.reserveFaceCount(total_face_count) # initialize to texel space vertex offsets. # 6 is for 6 vertices for each texel quad. heightmap_vertices = numpy.zeros( (width_minus_one * height_minus_one, 6, 3), dtype=numpy.float32) heightmap_vertices = heightmap_vertices + numpy.array( [[[0, base_height, 0], [0, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, 0], [0, base_height, 0]]], dtype=numpy.float32) offsetsz, offsetsx = numpy.mgrid[0:height_minus_one, 0:width - 1] offsetsx = numpy.array(offsetsx, numpy.float32).reshape( -1, 1) * texel_width offsetsz = numpy.array(offsetsz, numpy.float32).reshape( -1, 1) * texel_height # offsets for each texel quad heightmap_vertex_offsets = numpy.concatenate([ offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz ], 1) heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape( -1, 6, 3) # apply height data to y values heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, : -1].reshape( -1) heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1) heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[ 1:, 1:].reshape(-1) heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1) heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3) mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3) mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices mesh._vertex_count = heightmap_vertices.size // 3 mesh._face_count = heightmap_indices.size // 3 geo_width = width_minus_one * texel_width geo_height = height_minus_one * texel_height # bottom mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) # north and south walls for n in range(0, width_minus_one): x = n * texel_width nx = (n + 1) * texel_width hn0 = height_data[0, n] hn1 = height_data[0, n + 1] hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0) mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0) mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) # west and east walls for n in range(0, height_minus_one): y = n * texel_height ny = (n + 1) * texel_height hw0 = height_data[n, 0] hw1 = height_data[n + 1, 0] he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny) mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y) mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) mesh.calculateNormals(fast=True) scene_node.setMeshData(mesh.build()) return scene_node
def run(self): self._scene.acquireLock() for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break object_groups = [] if self._profile.getSettingValue("print_sequence") == "one_at_a_time": for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] ## Node can't be printed, so don't bother sending it. if getattr(node, "_outside_buildarea", False): continue children = node.getAllChildren() children.append(node) for child_node in children: if type(child_node) is SceneNode and child_node.getMeshData( ) and child_node.getMeshData().getVertices() is not None: temp_list.append(child_node) if temp_list: object_groups.append(temp_list) Job.yieldThread() if len(object_groups) == 0: Logger.log( "w", "No objects suitable for one at a time found, or no correct order found" ) else: temp_list = [] for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData( ) and node.getMeshData().getVertices() is not None: if not getattr(node, "_outside_buildarea", False): temp_list.append(node) Job.yieldThread() if temp_list: object_groups.append(temp_list) self._scene.releaseLock() if not object_groups: return self._sendSettings(self._profile) slice_message = self._socket.createMessage("cura.proto.Slice") for group in object_groups: group_message = slice_message.addRepeatedMessage("object_lists") if group[0].getParent().callDecoration("isGroup"): self._handlePerObjectSettings(group[0].getParent(), group_message) for current_object in group: mesh_data = current_object.getMeshData().getTransformed( current_object.getWorldTransformation()) obj = group_message.addRepeatedMessage("objects") obj.id = id(current_object) verts = numpy.array(mesh_data.getVertices()) verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj.vertices = verts self._handlePerObjectSettings(current_object, obj) Job.yieldThread() Logger.log("d", "Sending data to engine for slicing.") self._socket.sendMessage(slice_message) Logger.log("d", "Sending data to engine is completed") self.setResult(True)
def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When disabling the remove empty first layers setting, the minimum layer number will be a positive # value. In that case the first empty layers will be discarded and start processing layers from the # first layer with data. # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # simply offset all other layers so the lowest layer is always 0. It could happens that the first # raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = sys.maxsize negative_layers = 0 for layer in self._layers: if layer.repeatedMessageCount("path_segment") > 0: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # data. However, if there are empty layers in between, we compute them. if layer.id < min_layer_number: continue # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # and the model abs_layer_number = layer.id - min_layer_number if layer.id >= 0 and negative_layers != 0: abs_layer_number += (min_layer_number + negative_layers) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = manager.getActiveExtruderStacks() if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool( Application.getInstance().getPreferences().getValue( "view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler( ).getReaderForFile(file_name) if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead( file_name) == WorkspaceReader.PreReadResult.accepted: pass else: Logger.log( "w", "Could not find reader that was able to read the scene data for 3MF workspace" ) return WorkspaceReader.PreReadResult.failed machine_name = "" machine_type = "" # Check if there are any conflicts, so we can ask the user. archive = zipfile.ZipFile(file_name, "r") cura_file_names = [ name for name in archive.namelist() if name.startswith("Cura/") ] container_stack_files = [ name for name in cura_file_names if name.endswith(self._container_stack_suffix) ] self._resolve_strategies = { "machine": None, "quality_changes": None, "material": None } machine_conflict = False quality_changes_conflict = False for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) serialized = archive.open(container_stack_file).read().decode( "utf-8") if machine_name == "": machine_name = self._getMachineNameFromSerializedStack( serialized) stacks = self._container_registry.findContainerStacks( id=container_id) if stacks: # Check if there are any changes at all in any of the container stacks. id_list = self._getContainerIdListFromSerialized(serialized) for index, container_id in enumerate(id_list): if stacks[0].getContainer(index).getId() != container_id: machine_conflict = True Job.yieldThread() definition_container_files = [ name for name in cura_file_names if name.endswith(self._definition_container_suffix) ] for definition_container_file in definition_container_files: container_id = self._stripFileToId(definition_container_file) definitions = self._container_registry.findDefinitionContainers( id=container_id) if not definitions: definition_container = DefinitionContainer(container_id) definition_container.deserialize( archive.open(definition_container_file).read().decode( "utf-8")) if definition_container.getMetaDataEntry("type") != "extruder": machine_type = definition_container.getName() else: if definitions[0].getMetaDataEntry("type") != "extruder": machine_type = definitions[0].getName() Job.yieldThread() material_labels = [] material_conflict = False xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer( xml_material_profile).preferredSuffix if xml_material_profile: material_container_files = [ name for name in cura_file_names if name.endswith(self._material_container_suffix) ] for material_container_file in material_container_files: container_id = self._stripFileToId(material_container_file) materials = self._container_registry.findInstanceContainers( id=container_id) material_labels.append( self._getMaterialLabelFromSerialized( archive.open(material_container_file).read().decode( "utf-8"))) if materials and not materials[0].isReadOnly( ): # Only non readonly materials can be in conflict material_conflict = True Job.yieldThread() # Check if any quality_changes instance container is in conflict. instance_container_files = [ name for name in cura_file_names if name.endswith(self._instance_container_suffix) ] quality_name = "" quality_type = "" num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes num_user_settings = 0 for instance_container_file in instance_container_files: container_id = self._stripFileToId(instance_container_file) instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize( archive.open(instance_container_file).read().decode("utf-8")) container_type = instance_container.getMetaDataEntry("type") if container_type == "quality_changes": quality_name = instance_container.getName() num_settings_overriden_by_quality_changes += len( instance_container._instances) # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers( id=container_id) if quality_changes: # Check if there really is a conflict by comparing the values if quality_changes[0] != instance_container: quality_changes_conflict = True elif container_type == "quality": # If the quality name is not set (either by quality or changes, set it now) # Quality changes should always override this (as they are "on top") if quality_name == "": quality_name = instance_container.getName() quality_type = instance_container.getName() elif container_type == "user": num_user_settings += len(instance_container._instances) Job.yieldThread() num_visible_settings = 0 try: temp_preferences = Preferences() temp_preferences.readFromFile( io.TextIOWrapper(archive.open("Cura/preferences.cfg")) ) # We need to wrap it, else the archive parser breaks. visible_settings_string = temp_preferences.getValue( "general/visible_settings") if visible_settings_string is not None: num_visible_settings = len(visible_settings_string.split(";")) active_mode = temp_preferences.getValue("cura/active_mode") if not active_mode: active_mode = Preferences.getInstance().getValue( "cura/active_mode") except KeyError: # If there is no preferences file, it's not a workspace, so notify user of failure. Logger.log("w", "File %s is not a valid workspace.", file_name) return WorkspaceReader.PreReadResult.failed # Show the dialog, informing the user what is about to happen. self._dialog.setMachineConflict(machine_conflict) self._dialog.setQualityChangesConflict(quality_changes_conflict) self._dialog.setMaterialConflict(material_conflict) self._dialog.setNumVisibleSettings(num_visible_settings) self._dialog.setQualityName(quality_name) self._dialog.setQualityType(quality_type) self._dialog.setNumSettingsOverridenByQualityChanges( num_settings_overriden_by_quality_changes) self._dialog.setNumUserSettings(num_user_settings) self._dialog.setActiveMode(active_mode) self._dialog.setMachineName(machine_name) self._dialog.setMaterialLabels(material_labels) self._dialog.setMachineType(machine_type) self._dialog.setHasObjectsOnPlate( Application.getInstance().getPlatformActivity) self._dialog.show() # Block until the dialog is closed. self._dialog.waitForClose() if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled self._resolve_strategies = self._dialog.getResult() return WorkspaceReader.PreReadResult.accepted
def run(self) -> None: if self._build_plate_number is None: self.setResult(StartJobResult.Error) return stack = SteSlicerApplication.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) return # Don't slice if there is a setting with an error value. if SteSlicerApplication.getInstance().getMachineManager( ).stacksHaveErrors: self.setResult(StartJobResult.SettingError) return if SteSlicerApplication.getInstance().getBuildVolume().hasErrors(): self.setResult(StartJobResult.BuildPlateError) return # Don't slice if the buildplate or the nozzle type is incompatible with the materials if not SteSlicerApplication.getInstance().getMachineManager().variantBuildplateCompatible and \ not SteSlicerApplication.getInstance().getMachineManager().variantBuildplateUsable: self.setResult(StartJobResult.MaterialIncompatible) return for position, extruder_stack in stack.extruders.items(): material = extruder_stack.findContainer({"type": "material"}) if not extruder_stack.isEnabled: continue if material: if material.getMetaDataEntry("compatible") == False: self.setResult(StartJobResult.MaterialIncompatible) return # Don't slice if there is a per object setting with an error value. # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()): if not isinstance(node, SteSlicerSceneNode) or not node.isSelectable(): continue if self._checkStackForErrors(node.callDecoration("getStack")): self.setResult(StartJobResult.ObjectSettingError) return with self._scene.getSceneLock(): # Remove old layer data. # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData") and node.callDecoration( "getBuildPlateNumber") == self._build_plate_number: node.getParent().removeChild(node) break # Get the objects in their groups to print. object_groups = [] printing_mode = stack.getProperty("printing_mode", "value") if printing_mode == "classic": if stack.getProperty("print_sequence", "value") == "one_at_a_time": # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] # Node can't be printed, so don't bother sending it. if getattr(node, "_outside_buildarea", False): continue # Filter on current build plate build_plate_number = node.callDecoration( "getBuildPlateNumber") if build_plate_number is not None and build_plate_number != self._build_plate_number: continue children = node.getAllChildren() children.append(node) for child_node in children: if child_node.getMeshData( ) and child_node.getMeshData().getVertices( ) is not None: temp_list.append(child_node) if temp_list: object_groups.append(temp_list) Job.yieldThread() if len(object_groups) == 0: Logger.log( "w", "No objects suitable for one at a time found, or no correct order found" ) else: temp_list = [] has_printing_mesh = False # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration( "isSliceable") and node.getMeshData( ) and node.getMeshData().getVertices( ) is not None: per_object_stack = node.callDecoration("getStack") is_non_printing_mesh = False if per_object_stack: is_non_printing_mesh = any( per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS) # Find a reason not to add the node if node.callDecoration( "getBuildPlateNumber" ) != self._build_plate_number: continue if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh: continue temp_list.append(node) if not is_non_printing_mesh: has_printing_mesh = True Job.yieldThread() # If the list doesn't have any model with suitable settings then clean the list # otherwise CuraEngine will crash if not has_printing_mesh: temp_list.clear() if temp_list: object_groups.append(temp_list) elif printing_mode in ["cylindrical_full", "spherical_full"]: temp_list = [] has_printing_mesh = 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("isSliceable") and node.getMeshData( ) and node.getMeshData().getVertices() is not None: per_object_stack = node.callDecoration("getStack") is_non_printing_mesh = False if per_object_stack: is_non_printing_mesh = any( per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS) # Find a reason not to add the node if node.callDecoration("getBuildPlateNumber" ) != self._build_plate_number: continue if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh: continue temp_list.append(node) if not is_non_printing_mesh: has_printing_mesh = True Job.yieldThread() # If the list doesn't have any model with suitable settings then clean the list # otherwise CuraEngine will crash if not has_printing_mesh: temp_list.clear() else: self.setResult(StartJobResult.ObjectSettingError) return if temp_list and printing_mode in [ "cylindrical_full", "spherical_full" ]: cut_list = [] for node in temp_list: if printing_mode == "cylindrical_full": radius = SteSlicerApplication.getInstance( ).getGlobalContainerStack().getProperty( "cylindrical_mode_base_diameter", "value") / 2 height = node.getBoundingBox().height * 2 cutting_mesh = trimesh.primitives.Cylinder(radius=radius, height=height, sections=64) cutting_mesh.apply_transform( trimesh.transformations.rotation_matrix( numpy.pi / 2, [1, 0, 0])) elif printing_mode == "spherical_full": radius = SteSlicerApplication.getInstance( ).getGlobalContainerStack().getProperty( "spherical_mode_base_radius", "value") cutting_mesh = trimesh.primitives.Sphere(radius=radius, subdivisions=3) else: cutting_mesh = None mesh_data = node.getMeshData() if mesh_data.hasIndices(): faces = mesh_data.getIndices() else: num_verts = mesh_data.getVertexCount() faces = numpy.empty((int(num_verts / 3 + 1), 3), numpy.int32) for i in range(0, num_verts - 2, 3): faces[int(i / 3):] = [i, i + 1, i + 2] verts = mesh_data.getVertices() rot_scale = node.getWorldTransformation().getTransposed( ).getData()[0:3, 0:3] translate = node.getWorldTransformation().getData()[:3, 3] verts = verts.dot(rot_scale) verts += translate mesh = trimesh.Trimesh(vertices=verts, faces=faces) try: mesh.fill_holes() mesh.fix_normals() cutting_result = mesh.intersection(cutting_mesh, engine="scad") if cutting_result: cutting_result.fill_holes() cutting_result.fix_normals() data = MeshData.MeshData( vertices=cutting_result.vertices.astype('float32'), normals=cutting_result.face_normals.astype( 'float32'), indices=cutting_result.faces.astype('int64')) cutting_node = SteSlicerSceneNode( node.getParent(), no_setting_override=True) cutting_node.addDecorator( node.getDecorator(SettingOverrideDecorator)) except Exception as e: Logger.log("e", "Failed to intersect model! %s", e) cutting_result = cutting_mesh if cutting_result: cutting_result.fill_holes() cutting_result.fix_normals() data = MeshData.MeshData( vertices=cutting_result.vertices.astype('float32'), normals=cutting_result.face_normals.astype( 'float32'), indices=cutting_result.faces.astype('int64')) cutting_node = SteSlicerSceneNode( node.getParent(), no_setting_override=True) stack = cutting_node.callDecoration( "getStack" ) # Don't try to get the active extruder since it may be None anyway. if not stack: cutting_node.addDecorator( SettingOverrideDecorator()) stack = cutting_node.callDecoration("getStack") settings = stack.getTop() if not (settings.getInstance("support_mesh") and settings.getProperty("support_mesh", "value")): definition = stack.getSettingDefinition( "support_mesh") new_instance = SettingInstance( definition, settings) new_instance.setProperty("value", True, emit_signals=False) # Ensure that the state is not seen as a user state. new_instance.resetState() settings.addInstance(new_instance) if cutting_node is not None: cutting_node.setName("cut_" + node.getName()) cutting_node.setMeshData(data) cut_list.append(cutting_node) object_groups.append(cut_list) global_stack = SteSlicerApplication.getInstance( ).getGlobalContainerStack() if not global_stack: return extruders_enabled = { position: stack.isEnabled for position, stack in global_stack.extruders.items() } filtered_object_groups = [] has_model_with_disabled_extruders = False associated_disabled_extruders = set() for group in object_groups: stack = global_stack skip_group = False for node in group: # Only check if the printing extruder is enabled for printing meshes is_non_printing_mesh = node.callDecoration( "evaluateIsNonPrintingMesh") extruder_position = node.callDecoration( "getActiveExtruderPosition") if not is_non_printing_mesh and not extruders_enabled[ extruder_position]: skip_group = True has_model_with_disabled_extruders = True associated_disabled_extruders.add(extruder_position) if not skip_group: filtered_object_groups.append(group) if has_model_with_disabled_extruders: self.setResult(StartJobResult.ObjectsWithDisabledExtruder) associated_disabled_extruders = { str(c) for c in sorted( [int(p) + 1 for p in associated_disabled_extruders]) } self.setMessage(", ".join(associated_disabled_extruders)) return # There are cases when there is nothing to slice. This can happen due to one at a time slicing not being # able to find a possible sequence or because there are no objects on the build plate (or they are outside # the build volume) if not filtered_object_groups: self.setResult(StartJobResult.NothingToSlice) return self._buildGlobalSettingsMessage(stack) self._buildGlobalInheritsStackMessage(stack) # Build messages for extruder stacks # Send the extruder settings in the order of extruder positions. Somehow, if you send e.g. extruder 3 first, # then CuraEngine can slice with the wrong settings. This I think should be fixed in CuraEngine as well. extruder_stack_list = sorted(list(global_stack.extruders.items()), key=lambda item: int(item[0])) for _, extruder_stack in extruder_stack_list: self._buildExtruderMessage(extruder_stack) for group in filtered_object_groups: group_message = self._slice_message.addRepeatedMessage( "object_lists") if group[0].getParent() is not None and group[0].getParent( ).callDecoration("isGroup"): self._handlePerObjectSettings(group[0].getParent(), group_message) for object in group: mesh_data = object.getMeshData() rot_scale = object.getWorldTransformation().getTransposed( ).getData()[0:3, 0:3] translate = object.getWorldTransformation().getData()[:3, 3] # This effectively performs a limited form of MeshData.getTransformed that ignores normals. verts = mesh_data.getVertices() verts = verts.dot(rot_scale) if printing_mode == "classic": verts += translate # Convert from Y up axes to Z up axes. Equals a 90 degree rotation. verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj = group_message.addRepeatedMessage("objects") obj.id = id(object) obj.name = object.getName() indices = mesh_data.getIndices() if indices is not None: flat_verts = numpy.take(verts, indices.flatten(), axis=0) else: flat_verts = numpy.array(verts) obj.vertices = flat_verts self._handlePerObjectSettings(object, obj) Job.yieldThread() self.setResult(StartJobResult.Finished)
def read(self, file_name): Logger.log("d", "Preparing to load %s" % file_name) self._cancelled = False scene_node = SceneNode() # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no # real data to calculate it from. scene_node.getBoundingBox = self._getNullBoundingBox gcode_list = [] self._is_layers_in_file = False Logger.log("d", "Opening file %s" % file_name) self._extruder_offsets = self._extruderOffsets( ) # dict with index the extruder number. can be empty last_z = 0 with open(file_name, "r") as file: file_lines = 0 current_line = 0 for line in file: file_lines += 1 gcode_list.append(line) if not self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file.seek(0) file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0) self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing %s..." % file_name) current_position = self._position(0, 0, 0, [0]) current_path = [] for line in file: if self._cancelled: Logger.log("d", "Parsing %s cancelled" % file_name) return None current_line += 1 last_z = current_position.z if current_line % file_step == 0: self._message.setProgress( math.floor(current_line / file_lines * 100)) Job.yieldThread() if len(line) == 0: continue if line.find(self._type_keyword) == 0: type = line[len(self._type_keyword):].strip() if type == "WALL-INNER": self._layer_type = LayerPolygon.InsetXType elif type == "WALL-OUTER": self._layer_type = LayerPolygon.Inset0Type elif type == "SKIN": self._layer_type = LayerPolygon.SkinType elif type == "SKIRT": self._layer_type = LayerPolygon.SkirtType elif type == "SUPPORT": self._layer_type = LayerPolygon.SupportType elif type == "FILL": self._layer_type = LayerPolygon.InfillType if self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: try: layer_number = int(line[len(self._layer_keyword):]) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() self._layer_number = layer_number except: pass # This line is a comment. Ignore it (except for the layer_keyword) if line.startswith(";"): continue G = self._getInt(line, "G") if G is not None: current_position = self._processGCode( G, line, current_position, current_path) # < 2 is a heuristic for a movement only, that should not be counted as a layer if current_position.z > last_z and abs(current_position.z - last_z) < 2: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get( self._extruder_number, [0, 0])): current_path.clear() if not self._is_layers_in_file: self._layer_number += 1 continue if line.startswith("T"): T = self._getInt(line, "T") if T is not None: self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() current_position = self._processTCode( T, line, current_position, current_path) # "Flush" leftovers if not self._is_layers_in_file and len(current_path) > 1: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() material_color_map = numpy.zeros((10, 4), dtype=numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) Application.getInstance().getController().getScene( ).gcode_list = gcode_list Logger.log("d", "Finished parsing %s" % file_name) self._message.hide() if self._layer_number == 0: Logger.log("w", "File %s doesn't contain any valid layers" % file_name) settings = Application.getInstance().getGlobalContainerStack() machine_width = settings.getProperty("machine_width", "value") machine_depth = settings.getProperty("machine_depth", "value") if not self._center_is_zero: scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "Loaded %s" % file_name) if Preferences.getInstance().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate." ), lifetime=0) caution_message.show() # The "save/print" button's state is bound to the backend state. backend = Application.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def run(self) -> None: """Runs the job that initiates the slicing.""" if self._build_plate_number is None: self.setResult(StartJobResult.Error) return stack = CuraApplication.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) return # Don't slice if there is a setting with an error value. if CuraApplication.getInstance().getMachineManager().stacksHaveErrors: self.setResult(StartJobResult.SettingError) return if CuraApplication.getInstance().getBuildVolume().hasErrors(): self.setResult(StartJobResult.BuildPlateError) return # Wait for error checker to be done. while CuraApplication.getInstance().getMachineErrorChecker( ).needToWaitForResult: time.sleep(0.1) if CuraApplication.getInstance().getMachineErrorChecker().hasError: self.setResult(StartJobResult.SettingError) return # Don't slice if the buildplate or the nozzle type is incompatible with the materials if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \ not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable: self.setResult(StartJobResult.MaterialIncompatible) return for extruder_stack in stack.extruderList: material = extruder_stack.findContainer({"type": "material"}) if not extruder_stack.isEnabled: continue if material: if material.getMetaDataEntry("compatible") == False: self.setResult(StartJobResult.MaterialIncompatible) return # Don't slice if there is a per object setting with an error value. for node in DepthFirstIterator(self._scene.getRoot()): if not isinstance(node, CuraSceneNode) or not node.isSelectable(): continue if self._checkStackForErrors(node.callDecoration("getStack")): self.setResult(StartJobResult.ObjectSettingError) return # Remove old layer data. for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData") and node.callDecoration( "getBuildPlateNumber") == self._build_plate_number: # Singe we walk through all nodes in the scene, they always have a parent. cast(SceneNode, node.getParent()).removeChild(node) break # Get the objects in their groups to print. object_groups = [] if stack.getProperty("print_sequence", "value") == "one_at_a_time": for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] # Node can't be printed, so don't bother sending it. if getattr(node, "_outside_buildarea", False): continue # Filter on current build plate build_plate_number = node.callDecoration("getBuildPlateNumber") if build_plate_number is not None and build_plate_number != self._build_plate_number: continue children = node.getAllChildren() children.append(node) for child_node in children: mesh_data = child_node.getMeshData() if mesh_data and mesh_data.getVertices() is not None: temp_list.append(child_node) if temp_list: object_groups.append(temp_list) Job.yieldThread() if len(object_groups) == 0: Logger.log( "w", "No objects suitable for one at a time found, or no correct order found" ) else: temp_list = [] has_printing_mesh = False for node in DepthFirstIterator(self._scene.getRoot()): mesh_data = node.getMeshData() if node.callDecoration( "isSliceable") and mesh_data and mesh_data.getVertices( ) is not None: is_non_printing_mesh = bool( node.callDecoration("isNonPrintingMesh")) # Find a reason not to add the node if node.callDecoration( "getBuildPlateNumber") != self._build_plate_number: continue if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh: continue temp_list.append(node) if not is_non_printing_mesh: has_printing_mesh = True Job.yieldThread() # If the list doesn't have any model with suitable settings then clean the list # otherwise CuraEngine will crash if not has_printing_mesh: temp_list.clear() if temp_list: object_groups.append(temp_list) global_stack = CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return extruders_enabled = [ stack.isEnabled for stack in global_stack.extruderList ] filtered_object_groups = [] has_model_with_disabled_extruders = False associated_disabled_extruders = set() for group in object_groups: stack = global_stack skip_group = False for node in group: # Only check if the printing extruder is enabled for printing meshes is_non_printing_mesh = node.callDecoration( "evaluateIsNonPrintingMesh") extruder_position = int( node.callDecoration("getActiveExtruderPosition")) if not is_non_printing_mesh and not extruders_enabled[ extruder_position]: skip_group = True has_model_with_disabled_extruders = True associated_disabled_extruders.add(extruder_position) if not skip_group: filtered_object_groups.append(group) if has_model_with_disabled_extruders: self.setResult(StartJobResult.ObjectsWithDisabledExtruder) associated_disabled_extruders = { p + 1 for p in associated_disabled_extruders } self.setMessage(", ".join( map(str, sorted(associated_disabled_extruders)))) return # There are cases when there is nothing to slice. This can happen due to one at a time slicing not being # able to find a possible sequence or because there are no objects on the build plate (or they are outside # the build volume) if not filtered_object_groups: self.setResult(StartJobResult.NothingToSlice) return self._buildGlobalSettingsMessage(stack) self._buildGlobalInheritsStackMessage(stack) # Build messages for extruder stacks for extruder_stack in global_stack.extruderList: self._buildExtruderMessage(extruder_stack) for group in filtered_object_groups: group_message = self._slice_message.addRepeatedMessage( "object_lists") parent = group[0].getParent() if parent is not None and parent.callDecoration("isGroup"): self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message) for object in group: mesh_data = object.getMeshData() if mesh_data is None: continue rot_scale = object.getWorldTransformation().getTransposed( ).getData()[0:3, 0:3] translate = object.getWorldTransformation().getData()[:3, 3] # This effectively performs a limited form of MeshData.getTransformed that ignores normals. verts = mesh_data.getVertices() verts = verts.dot(rot_scale) verts += translate # Convert from Y up axes to Z up axes. Equals a 90 degree rotation. verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj = group_message.addRepeatedMessage("objects") obj.id = id(object) obj.name = object.getName() indices = mesh_data.getIndices() if indices is not None: flat_verts = numpy.take(verts, indices.flatten(), axis=0) else: flat_verts = numpy.array(verts) obj.vertices = flat_verts self._handlePerObjectSettings(cast(CuraSceneNode, object), obj) Job.yieldThread() self.setResult(StartJobResult.Finished)
def processCliStream(self, stream: str) -> Optional[SteSlicerSceneNode]: Logger.log("d", "Preparing to load CLI") self._cancelled = False self._setPrintSettings() self._is_layers_in_file = False scene_node = SteSlicerSceneNode() gcode_list = [] self._writeStartCode(gcode_list) gcode_list.append(";LAYER_COUNT\n") # Reading starts here file_lines = 0 current_line = 0 for line in stream.split("\n"): file_lines += 1 if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing CLI"), lifetime=0, title=catalog.i18nc("@info:title", "CLI Details")) assert(self._message is not None) # use for typing purposes self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing CLI...") self._position = Position(0, 0, 0, 0, 0, 1, 0, [0]) self._gcode_position = Position(0, 0, 0, 0, 0, 0, 0, [0]) current_path = [] # type: List[List[float]] geometry_start = False for line in stream.split("\n"): if self._cancelled: Logger.log("d", "Parsing CLI file cancelled") return None current_line += 1 if current_line % file_step == 0: self._message.setProgress(math.floor( current_line / file_lines * 100)) Job.yieldThread() if len(line) == 0: continue if line == "$$GEOMETRYSTART": geometry_start = True continue if not geometry_start: continue if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword: try: layer_height = float(line[len(self._layer_keyword):]) self._current_layer_thickness = layer_height - self._current_layer_height if self._current_layer_thickness > 0.4: self._current_layer_thickness = 0.2 self._current_layer_height = layer_height self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get( self._extruder_number, [0, 0])) current_path.clear() # Start the new layer at the end position of the last layer current_path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b, self._position.c, self._position.f, self._position.e[self._extruder_number], LayerPolygon.MoveCombingType]) self._layer_number += 1 gcode_list.append(";LAYER:%s\n" % self._layer_number) except: pass if line.find(self._body_type_keyword) == 0: self._layer_type = LayerPolygon.Inset0Type if line.find(self._support_type_keyword) == 0: self._layer_type = LayerPolygon.SupportType if line.find(self._perimeter_type_keyword) == 0: self._layer_type = LayerPolygon.Inset0Type if line.find(self._skin_type_keyword) == 0: self._layer_type = LayerPolygon.SkinType if line.find(self._infill_type_keyword) == 0: self._layer_type = LayerPolygon.InfillType # Comment line if line.startswith("//"): continue # Polyline processing self.processPolyline(line, current_path, gcode_list) # "Flush" leftovers. Last layer paths are still stored if len(current_path) > 1: if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() layer_count_idx = gcode_list.index(";LAYER_COUNT\n") if layer_count_idx > 0: gcode_list[layer_count_idx] = ";LAYER_COUNT:%s\n" % self._layer_number end_gcode = self._global_stack.getProperty( "machine_end_gcode", "value") gcode_list.append(end_gcode + "\n") material_color_map = numpy.zeros((8, 4), dtype=numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0] material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0] material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0] material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0] material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0] material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) # gcode_dict stores gcode_lists for a number of build plates. active_build_plate_id = SteSlicerApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_dict = {active_build_plate_id: gcode_list} # type: ignore #Because gcode_dict is generated dynamically. SteSlicerApplication.getInstance().getController().getScene().gcode_dict = gcode_dict Logger.log("d", "Finished parsing CLI file") self._message.hide() if self._layer_number == 0: Logger.log("w", "File doesn't contain any valid layers") if not self._global_stack.getProperty("machine_center_is_zero", "value"): machine_width = self._global_stack.getProperty( "machine_width", "value") machine_depth = self._global_stack.getProperty( "machine_depth", "value") scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "CLI loading finished") if SteSlicerApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), lifetime=0, title=catalog.i18nc("@info:title", "G-code Details")) caution_message.show() backend = SteSlicerApplication.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def run(self) -> None: 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 Objects")) status_message.show() scene = Application.getInstance().getController().getScene() total_progress = len(self._objects) * self._count current_progress = 0 global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack is None: return # We can't do anything in this case. machine_width = global_container_stack.getProperty( "machine_width", "value") machine_depth = global_container_stack.getProperty( "machine_depth", "value") root = scene.getRoot() scale = 0.5 arranger = Arrange.create(x=machine_width, y=machine_depth, scene_root=root, scale=scale, min_offset=self._min_offset) processed_nodes = [] # type: List[SceneNode] nodes = [] not_fit_count = 0 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") or current_node.getParent().callDecoration("isSliceable")): current_node = current_node.getParent() if current_node in processed_nodes: continue processed_nodes.append(current_node) node_too_big = False if node.getBoundingBox( ).width < machine_width or node.getBoundingBox( ).depth < machine_depth: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode( current_node, min_offset=self._min_offset, scale=scale) else: node_too_big = True found_solution_for_all = True arranger.resetLastPriority() for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. new_node = copy.deepcopy(node) solution_found = False if not node_too_big: if offset_shape_arr is not None and hull_shape_arr is not None: solution_found = arranger.findNodePlacement( new_node, offset_shape_arr, hull_shape_arr) else: # The node has no shape, so no need to arrange it. The solution is simple: Do nothing. solution_found = True if node_too_big or not solution_found: found_solution_for_all = False new_location = new_node.getPosition() new_location = new_location.set(z=-not_fit_count * 20) new_node.setPosition(new_location) not_fit_count += 1 # 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 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() x, y = 200, 200 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). # We also skip possibilities by slicing through the possibilities (step = 10) try_placement = True current_build_plate_number = 0 # always start with the first one # # Only for first build plate # if last_size == size and last_build_plate_number == current_build_plate_number: # # This optimization works if many of the objects have the same size # # Continue with same build plate number # start_priority = last_priority # else: # start_priority = 0 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(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 arranger.place( x, y, hull_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()
def processGCodeFile(self, file_name): Logger.log("d", "Preparing to load %s" % file_name) self._cancelled = False # We obtain the filament diameter from the selected printer to calculate line widths self._filament_diameter = Application.getInstance( ).getGlobalContainerStack().getProperty("material_diameter", "value") scene_node = SceneNode() # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no # real data to calculate it from. scene_node.getBoundingBox = self._getNullBoundingBox gcode_list = [] self._is_layers_in_file = False Logger.log("d", "Opening file %s" % file_name) self._extruder_offsets = self._extruderOffsets( ) # dict with index the extruder number. can be empty with open(file_name, "r") as file: file_lines = 0 current_line = 0 for line in file: file_lines += 1 gcode_list.append(line) if not self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file.seek(0) file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0, title=catalog.i18nc( "@info:title", "G-code Details")) self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing %s..." % file_name) current_position = self._position(0, 0, 0, 0, [0]) current_path = [] min_layer_number = 0 negative_layers = 0 previous_layer = 0 for line in file: if self._cancelled: Logger.log("d", "Parsing %s cancelled" % file_name) return None current_line += 1 if current_line % file_step == 0: self._message.setProgress( math.floor(current_line / file_lines * 100)) Job.yieldThread() if len(line) == 0: continue if line.find(self._type_keyword) == 0: type = line[len(self._type_keyword):].strip() if type == "WALL-INNER": self._layer_type = LayerPolygon.InsetXType elif type == "WALL-OUTER": self._layer_type = LayerPolygon.Inset0Type elif type == "SKIN": self._layer_type = LayerPolygon.SkinType elif type == "SKIRT": self._layer_type = LayerPolygon.SkirtType elif type == "SUPPORT": self._layer_type = LayerPolygon.SupportType elif type == "FILL": self._layer_type = LayerPolygon.InfillType else: Logger.log( "w", "Encountered a unknown type (%s) while parsing g-code.", type) # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder if self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: try: layer_number = int(line[len(self._layer_keyword):]) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob if layer_number < min_layer_number: min_layer_number = layer_number if layer_number < 0: layer_number += abs(min_layer_number) negative_layers += 1 else: layer_number += negative_layers # In case there is a gap in the layer count, empty layers are created for empty_layer in range(previous_layer + 1, layer_number): self._createEmptyLayer(empty_layer) self._layer_number = layer_number previous_layer = layer_number except: pass # This line is a comment. Ignore it (except for the layer_keyword) if line.startswith(";"): continue G = self._getInt(line, "G") if G is not None: # When find a movement, the new posistion is calculated and added to the current_path, but # don't need to create a polygon until the end of the layer current_position = self.processGCode( G, line, current_position, current_path) continue # When changing the extruder, the polygon with the stored paths is computed if line.startswith("T"): T = self._getInt(line, "T") if T is not None: self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() current_position = self.processTCode( T, line, current_position, current_path) if line.startswith("M"): M = self._getInt(line, "M") self.processMCode(M, line, current_position, current_path) # "Flush" leftovers. Last layer paths are still stored if len(current_path) > 1: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() material_color_map = numpy.zeros((10, 4), dtype=numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) Application.getInstance().getController().getScene( ).gcode_list = gcode_list Logger.log("d", "Finished parsing %s" % file_name) self._message.hide() if self._layer_number == 0: Logger.log("w", "File %s doesn't contain any valid layers" % file_name) settings = Application.getInstance().getGlobalContainerStack() machine_width = settings.getProperty("machine_width", "value") machine_depth = settings.getProperty("machine_depth", "value") if not self._center_is_zero: scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "Loaded %s" % file_name) if Preferences.getInstance().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate." ), lifetime=0, title=catalog.i18nc( "@info:title", "G-code Details")) caution_message.show() # The "save/print" button's state is bound to the backend state. backend = Application.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def run(self): start_time = time() if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break if self._abort_requested: if self._progress: self._progress.hide() return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # In the future, line_thicknesses should be given by CuraEngine as well. # Currently the infill layer thickness also translates to line width line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list( manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance( ).getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def run(self): stack = Application.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) return # Don't slice if there is a setting with an error value. if Application.getInstance().getMachineManager().stacksHaveErrors: self.setResult(StartJobResult.SettingError) return if Application.getInstance().getBuildVolume().hasErrors(): self.setResult(StartJobResult.BuildPlateError) return for extruder_stack in ExtruderManager.getInstance( ).getMachineExtruders(stack.getId()): material = extruder_stack.findContainer({"type": "material"}) if material: if material.getMetaDataEntry("compatible") == False: self.setResult(StartJobResult.MaterialIncompatible) return # Don't slice if there is a per object setting with an error value. for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is not SceneNode or not node.isSelectable(): continue if self._checkStackForErrors(node.callDecoration("getStack")): self.setResult(StartJobResult.ObjectSettingError) return with self._scene.getSceneLock(): # Remove old layer data. for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break # Get the objects in their groups to print. object_groups = [] if stack.getProperty("print_sequence", "value") == "one_at_a_time": for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] # Node can't be printed, so don't bother sending it. if getattr(node, "_outside_buildarea", False): continue children = node.getAllChildren() children.append(node) for child_node in children: if type(child_node ) is SceneNode and child_node.getMeshData( ) and child_node.getMeshData().getVertices( ) is not None: temp_list.append(child_node) if temp_list: object_groups.append(temp_list) Job.yieldThread() if len(object_groups) == 0: Logger.log( "w", "No objects suitable for one at a time found, or no correct order found" ) else: temp_list = [] for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData( ) and node.getMeshData().getVertices() is not None: if not getattr(node, "_outside_buildarea", False) or getattr( node, "_non_printing_mesh", False): temp_list.append(node) Job.yieldThread() if temp_list: object_groups.append(temp_list) # There are cases when there is nothing to slice. This can happen due to one at a time slicing not being # able to find a possible sequence or because there are no objects on the build plate (or they are outside # the build volume) if not object_groups: self.setResult(StartJobResult.NothingToSlice) return self._buildGlobalSettingsMessage(stack) self._buildGlobalInheritsStackMessage(stack) # Build messages for extruder stacks for extruder_stack in ExtruderManager.getInstance( ).getMachineExtruders(stack.getId()): self._buildExtruderMessage(extruder_stack) for group in object_groups: group_message = self._slice_message.addRepeatedMessage( "object_lists") if group[0].getParent().callDecoration("isGroup"): self._handlePerObjectSettings(group[0].getParent(), group_message) for object in group: mesh_data = object.getMeshData() rot_scale = object.getWorldTransformation().getTransposed( ).getData()[0:3, 0:3] translate = object.getWorldTransformation().getData()[:3, 3] # This effectively performs a limited form of MeshData.getTransformed that ignores normals. verts = mesh_data.getVertices() verts = verts.dot(rot_scale) verts += translate # Convert from Y up axes to Z up axes. Equals a 90 degree rotation. verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj = group_message.addRepeatedMessage("objects") obj.id = id(object) indices = mesh_data.getIndices() if indices is not None: flat_verts = numpy.take(verts, indices.flatten(), axis=0) else: flat_verts = numpy.array(verts) obj.vertices = flat_verts self._handlePerObjectSettings(object, obj) Job.yieldThread() self.setResult(StartJobResult.Finished)
def run(self): Job.yieldThread() self.setResult(self._writer.write(self._stream, self._data, self._mode))
def read(self, file_name): result = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, 'r') try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) if len(objects) == 0: Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) return None for object in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in object.mesh.vertices.vertex: for vertex in object.findall(".//3mf:vertex", self._namespaces): vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) Job.yieldThread() triangles = object.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2]) Job.yieldThread() #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces) if transformation: transformation = transformation[0] if transformation.get("transform"): splitted_transformation = transformation.get("transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0,0] = splitted_transformation[0] temp_mat._data[1,0] = splitted_transformation[1] temp_mat._data[2,0] = splitted_transformation[2] temp_mat._data[0,1] = splitted_transformation[3] temp_mat._data[1,1] = splitted_transformation[4] temp_mat._data[2,1] = splitted_transformation[5] temp_mat._data[0,2] = splitted_transformation[6] temp_mat._data[1,2] = splitted_transformation[7] temp_mat._data[2,2] = splitted_transformation[8] # Translation temp_mat._data[0,3] = splitted_transformation[9] temp_mat._data[1,3] = splitted_transformation[10] temp_mat._data[2,3] = splitted_transformation[11] node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3))) temp_quaternion = Quaternion() temp_quaternion.setByMatrix(temp_mat) node.setOrientation(temp_quaternion) # Magical scale extraction S2 = temp_mat.getTransposed().multiply(temp_mat) scale_x = math.sqrt(S2.at(0,0)) scale_y = math.sqrt(S2.at(1,1)) scale_z = math.sqrt(S2.at(2,2)) node.setScale(Vector(scale_x,scale_y,scale_z)) # We use a different coordinate frame, so rotate. #rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0)) #node.rotate(rotation) result.addChild(node) Job.yieldThread() #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e" ,"exception occured in 3mf reader: %s" , e) return result
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: settings = self._buildReplacementTokens(stack) # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend start_gcode = settings["machine_start_gcode"] bed_temperature_settings = [ "material_bed_temperature", "material_bed_temperature_layer_0" ] # match {setting} as well as {setting, extruder_nr} pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) settings["material_bed_temp_prepend"] = re.search( pattern, start_gcode) == None print_temperature_settings = [ "material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature" ] # match {setting} as well as {setting, extruder_nr} pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) settings["material_print_temp_prepend"] = re.search( pattern, start_gcode) == None # Replace the setting tokens in start and end g-code. # Use values from the first used extruder by default so we get the expected temperatures initial_extruder_stack = SteSlicerApplication.getInstance( ).getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty( "extruder_nr", "value") settings["machine_start_gcode"] = self._expandGcodeTokens( settings["machine_start_gcode"], initial_extruder_nr) settings["machine_end_gcode"] = self._expandGcodeTokens( settings["machine_end_gcode"], initial_extruder_nr) printing_mode = settings["printing_mode"] if printing_mode in ["classic", "cylindrical_full"]: settings["infill_extruder_nr"] = settings[ "classic_infill_extruder_nr"] settings["speed_infill"] = settings["speed_infill_classic"] settings["speed_wall_0"] = settings["speed_wall_0_classic"] settings["speed_wall_x"] = settings["speed_wall_x_classic"] settings["speed_roofing"] = settings["speed_roofing_classic"] settings["speed_topbottom"] = settings["speed_topbottom_classic"] settings["speed_support_infill"] = settings[ "speed_support_infill_classic"] settings["speed_travel"] = settings["speed_travel_classic"] settings["speed_print_layer_0"] = settings[ "speed_print_layer_0_classic"] settings["speed_travel_layer_0"] = settings[ "speed_travel_layer_0_classic"] settings["cool_fan_enabled"] = settings["cool_fan_enabled_classic"] settings["cool_fan_speed_min"] = settings[ "cool_fan_speed_min_classic"] settings["cool_fan_speed_max"] = settings[ "cool_fan_speed_max_classic"] # Add all sub-messages for each individual setting. for key, value in settings.items(): setting_message = self._slice_message.getMessage( "global_settings").addRepeatedMessage("settings") setting_message.name = key setting_message.value = str(value).encode("utf-8") Job.yieldThread()
def run(self): if self._build_plate_number is None: self.setResult(StartJobResult.Error) return stack = Application.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) return # Don't slice if there is a setting with an error value. if Application.getInstance().getMachineManager().stacksHaveErrors: self.setResult(StartJobResult.SettingError) return if Application.getInstance().getBuildVolume().hasErrors(): self.setResult(StartJobResult.BuildPlateError) return # Don't slice if the buildplate or the nozzle type is incompatible with the materials if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \ not Application.getInstance().getMachineManager().variantBuildplateUsable: self.setResult(StartJobResult.MaterialIncompatible) return for position, extruder_stack in stack.extruders.items(): material = extruder_stack.findContainer({"type": "material"}) if not extruder_stack.isEnabled: continue if material: if material.getMetaDataEntry("compatible") == False: self.setResult(StartJobResult.MaterialIncompatible) return # Don't slice if there is a per object setting with an error value. for node in DepthFirstIterator(self._scene.getRoot()): if not isinstance(node, CuraSceneNode) or not node.isSelectable(): continue if self._checkStackForErrors(node.callDecoration("getStack")): self.setResult(StartJobResult.ObjectSettingError) return with self._scene.getSceneLock(): # Remove old layer data. for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData") and node.callDecoration( "getBuildPlateNumber") == self._build_plate_number: node.getParent().removeChild(node) break # Get the objects in their groups to print. object_groups = [] if stack.getProperty("print_sequence", "value") == "one_at_a_time": for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] # Node can't be printed, so don't bother sending it. if getattr(node, "_outside_buildarea", False): continue # Filter on current build plate build_plate_number = node.callDecoration( "getBuildPlateNumber") if build_plate_number is not None and build_plate_number != self._build_plate_number: continue children = node.getAllChildren() children.append(node) for child_node in children: if child_node.getMeshData() and child_node.getMeshData( ).getVertices() is not None: temp_list.append(child_node) if temp_list: object_groups.append(temp_list) Job.yieldThread() if len(object_groups) == 0: Logger.log( "w", "No objects suitable for one at a time found, or no correct order found" ) else: temp_list = [] has_printing_mesh = False for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isSliceable") and node.getMeshData( ) and node.getMeshData().getVertices() is not None: per_object_stack = node.callDecoration("getStack") is_non_printing_mesh = False if per_object_stack: is_non_printing_mesh = any( per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS) # Find a reason not to add the node if node.callDecoration("getBuildPlateNumber" ) != self._build_plate_number: continue if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh: continue temp_list.append(node) if not is_non_printing_mesh: has_printing_mesh = True Job.yieldThread() #If the list doesn't have any model with suitable settings then clean the list # otherwise CuraEngine will crash if not has_printing_mesh: temp_list.clear() if temp_list: object_groups.append(temp_list) extruders_enabled = { position: stack.isEnabled for position, stack in Application.getInstance(). getGlobalContainerStack().extruders.items() } filtered_object_groups = [] for group in object_groups: stack = Application.getInstance().getGlobalContainerStack() skip_group = False for node in group: if not extruders_enabled[node.callDecoration( "getActiveExtruderPosition")]: skip_group = True break if not skip_group: filtered_object_groups.append(group) # There are cases when there is nothing to slice. This can happen due to one at a time slicing not being # able to find a possible sequence or because there are no objects on the build plate (or they are outside # the build volume) if not filtered_object_groups: self.setResult(StartJobResult.NothingToSlice) return self._buildGlobalSettingsMessage(stack) self._buildGlobalInheritsStackMessage(stack) # Build messages for extruder stacks for extruder_stack in ExtruderManager.getInstance( ).getMachineExtruders(stack.getId()): self._buildExtruderMessage(extruder_stack) for group in filtered_object_groups: group_message = self._slice_message.addRepeatedMessage( "object_lists") if group[0].getParent() is not None and group[0].getParent( ).callDecoration("isGroup"): self._handlePerObjectSettings(group[0].getParent(), group_message) for object in group: mesh_data = object.getMeshData() rot_scale = object.getWorldTransformation().getTransposed( ).getData()[0:3, 0:3] translate = object.getWorldTransformation().getData()[:3, 3] # This effectively performs a limited form of MeshData.getTransformed that ignores normals. verts = mesh_data.getVertices() verts = verts.dot(rot_scale) verts += translate # Convert from Y up axes to Z up axes. Equals a 90 degree rotation. verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj = group_message.addRepeatedMessage("objects") obj.id = id(object) indices = mesh_data.getIndices() if indices is not None: flat_verts = numpy.take(verts, indices.flatten(), axis=0) else: flat_verts = numpy.array(verts) obj.vertices = flat_verts self._handlePerObjectSettings(object, obj) Job.yieldThread() self.setResult(StartJobResult.Finished)
def read(self, file_name): result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) if len(objects) == 0: Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) return None for entry in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in entry.mesh.vertices.vertex: for vertex in entry.findall(".//3mf:vertex", self._namespaces): vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) Job.yieldThread() triangles = entry.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2]) Job.yieldThread() # Rotate the model; We use a different coordinate frame. rotation = Matrix() rotation.setByRotationAxis(-0.5 * math.pi, Vector(1,0,0)) mesh = mesh.getTransformed(rotation) #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces) if transformation: transformation = transformation[0] try: if transformation.get("transform"): splitted_transformation = transformation.get("transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0,0] = splitted_transformation[0] temp_mat._data[1,0] = splitted_transformation[1] temp_mat._data[2,0] = splitted_transformation[2] temp_mat._data[0,1] = splitted_transformation[3] temp_mat._data[1,1] = splitted_transformation[4] temp_mat._data[2,1] = splitted_transformation[5] temp_mat._data[0,2] = splitted_transformation[6] temp_mat._data[1,2] = splitted_transformation[7] temp_mat._data[2,2] = splitted_transformation[8] # Translation temp_mat._data[0,3] = splitted_transformation[9] temp_mat._data[1,3] = splitted_transformation[10] temp_mat._data[2,3] = splitted_transformation[11] node.setTransformation(temp_mat) except AttributeError: pass # Empty list was found. Getting transformation is not possible result.addChild(node) Job.yieldThread() #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e" ,"exception occured in 3mf reader: %s" , e) return result
def _generateSceneNode(self, file_name, offset, peak_height, slopeHeight, closeTopButtonFace, reversePathToration, splitWord): Job.yieldThread() if not splitWord: scene_node = SceneNode() mesh = MeshBuilder() else: scene_node = [] areaTop = 0 areaBottom = 0 pathDetectNotEqual = False for subPaths in self._paths: if splitWord: mesh = MeshBuilder() pros = self._vu._gen_offset_paths(subPaths) clocks = [] for path in subPaths: reverse = file_name.endswith('.nc') if reverse == (not reversePathToration): path.reverse() (clock, holePoint) = self._vu.clockWish(path) clocks.append(clock) lastPaths = self._vu._exec_offset(pros, clocks, 0) if offset != 0: currPaths = lastPaths self._vu.addSidFaces(lastPaths, currPaths, clocks, mesh, 0, peak_height - slopeHeight) currPaths = self._vu._exec_offset(pros, clocks, offset) pathDetectNotEqual = self._vu.addSidFaces( lastPaths, currPaths, clocks, mesh, peak_height - slopeHeight, peak_height) else: currPaths = lastPaths self._vu.addSidFaces(lastPaths, currPaths, clocks, mesh, 0, peak_height) if closeTopButtonFace: areaTop += self._vu.addTopButtonFace(True, currPaths, mesh, peak_height) areaBottom += self._vu.addTopButtonFace( False, lastPaths, mesh, 0) if splitWord: mesh.calculateNormals(fast=True) _sceneNode = SceneNode() _sceneNode.setMeshData(mesh.build()) scene_node.append(_sceneNode) if not splitWord: mesh.calculateNormals(fast=True) scene_node.setMeshData(mesh.build()) else: scene_node.reverse() if closeTopButtonFace: if pathDetectNotEqual: m = Message(i18n_catalog.i18nc( '@info:status', 'Top/Buttom area :{} mm\xc2\xb2 ,{} mm\xc2\xb2\n There may be broken, please reduce the offset', round(areaTop, 2), round(areaBottom, 2)), lifetime=0) else: m = Message(i18n_catalog.i18nc( '@info:status', 'Top/Buttom area :{} mm\xc2\xb2 ,{} mm\xc2\xb2', round(areaTop, 2), round(areaBottom, 2)), lifetime=0) m.addAction( 'regenerate', i18n_catalog.i18nc('@action:button', 'regenerate'), 'regenerate', i18n_catalog.i18nc('@info:tooltip', 'Regenerating model')) m._filename = file_name m.actionTriggered.connect(self._onMessageActionTriggered) m.show() return scene_node
def run(self): loading_message = Message(i18n_catalog.i18nc( "@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime=0, dismissable=False) loading_message.setProgress(-1) loading_message.show() Job.yieldThread( ) # Yield to any other thread that might want to do something else. node = None try: begin_time = time.time() node = self._handler.read(self._filename) end_time = time.time() Logger.log("d", "Loading mesh took %s seconds", end_time - begin_time) except: Logger.logException("e", "Exception in mesh loader") if not node: loading_message.hide() result_message = Message( i18n_catalog.i18nc("@info:status", "Failed to load <filename>{0}</filename>", self._filename)) result_message.show() return if node.getMeshData(): node.getMeshData().setFileName(self._filename) # 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 node._resetAABB() bounding_box = node.getBoundingBox() timeout_counter = 0 #As the calculation of the bounding box is in a seperate thread it might be that it's not done yet. while bounding_box.width == 0 or bounding_box.height == 0 or bounding_box.depth == 0: bounding_box = node.getBoundingBox() time.sleep(0.1) timeout_counter += 1 if timeout_counter > 10: break if max_bounds.width < bounding_box.width or max_bounds.height < bounding_box.height or max_bounds.depth < bounding_box.depth: scale_factor_width = max_bounds.width / bounding_box.width scale_factor_height = max_bounds.height / bounding_box.height scale_factor_depth = max_bounds.depth / bounding_box.depth scale_factor = min(scale_factor_width, scale_factor_height, scale_factor_depth) scale_vector = Vector(scale_factor, scale_factor, scale_factor) display_scale_factor = scale_factor * 100 if Preferences.getInstance().getValue( "mesh/scale_to_fit") == True: scale_message = Message( i18n_catalog.i18nc( "@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor))) try: node.scale(scale_vector) scale_message.show() except Exception as e: print(e) self.setResult(node) loading_message.hide()
def run(self): if self._handler is None: Logger.log("e", "FileHandler was not set.") return None reader = self._handler.getReaderForFile(self._filename) if not reader: result_message = Message(i18n_catalog.i18nc( "@info:status", "Cannot open file type <filename>{0}</filename>", self._filename), lifetime=0) result_message.show() return # Give the plugin a chance to display a dialog before showing the loading UI try: pre_read_result = reader.preRead(self._filename) except: Logger.logException("e", "Failed to pre-read the file %s", self._filename) pre_read_result = MeshReader.PreReadResult.failed if pre_read_result != MeshReader.PreReadResult.accepted: if pre_read_result == MeshReader.PreReadResult.failed: result_message = Message(i18n_catalog.i18nc( "@info:status", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0) result_message.show() return self._loading_message = Message(i18n_catalog.i18nc( "@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime=0, dismissable=False) self._loading_message.setProgress(-1) self._loading_message.show() Job.yieldThread( ) # Yield to any other thread that might want to do something else. try: begin_time = time.time() self.setResult(self._handler.readerRead(reader, self._filename)) end_time = time.time() Logger.log("d", "Loading file took %0.1f seconds", end_time - begin_time) except: Logger.logException("e", "Exception occurred while loading file %s", self._filename) finally: if self._result is None: self._loading_message.hide() result_message = Message(i18n_catalog.i18nc( "@info:status", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0) result_message.show() return self._loading_message.hide()
def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. It could happens that # the first raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = 0 negative_layers = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # Negative layers are offset by the minimum layer number, but the positive layers are just # offset by the number of negative layers so there is no layer gap between raft and model abs_layer_number = layer.id + abs( min_layer_number ) if layer.id < 0 else layer.id + negative_layers layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. global_container_stack = Application.getInstance( ).getGlobalContainerStack() half_outer_wall_thickness = global_container_stack.getProperty( "wall_line_width_0", "value") / 2 # Adjust layer data to show Raft line type, if it is enabled if global_container_stack.getProperty("blackbelt_raft", "value"): raft_thickness = global_container_stack.getProperty( "blackbelt_raft_thickness", "value") extrusion_started = False for index, segment_type in enumerate(line_types): if points[index + 1][ 1] <= half_outer_wall_thickness + raft_thickness: if segment_type in [ LayerPolygon.LayerPolygon.Inset0Type, LayerPolygon.LayerPolygon.InsetXType ]: line_types[ index] = LayerPolygon.LayerPolygon.SkirtType extrusion_started = True elif extrusion_started: break # Adjust layer data to show Belt Wall feed rate, if it is enabled if global_container_stack.getProperty( "blackbelt_belt_wall_enabled", "value"): belt_wall_feedrate = global_container_stack.getProperty( "blackbelt_belt_wall_speed", "value") belt_wall_indices = [] for index, point in enumerate(points): if point[1] <= half_outer_wall_thickness: if last_point_hit_wall and line_feedrates[ index - 1] > belt_wall_feedrate: belt_wall_indices.append(index) last_point_hit_wall = True else: last_point_hit_wall = False dimensionality = points.shape[1] edited_points = points.flatten() line_types = line_types.flatten() line_widths = line_widths.flatten() line_thicknesses = line_thicknesses.flatten() line_feedrates = line_feedrates.flatten() for index in reversed(belt_wall_indices): edited_points = numpy.insert( edited_points, dimensionality * (index), numpy.append(points[index - 1], points[index])) line_types = numpy.insert(line_types, index, [line_types[index - 1]] * 2) line_widths = numpy.insert( line_widths, index, [line_widths[index - 1]] * 2) line_thicknesses = numpy.insert( line_thicknesses, index, [line_thicknesses[index - 1]] * 2) line_feedrates = numpy.insert(line_feedrates, index - 1, [belt_wall_feedrate] * 2) # Fix shape of adjusted data if polygon.point_type == 0: points = edited_points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: points = edited_points.reshape((-1, 3)) line_types = line_types.reshape((-1, 1)) line_widths = line_widths.reshape((-1, 1)) line_thicknesses = line_thicknesses.reshape((-1, 1)) line_feedrates = line_feedrates.reshape((-1, 1)) # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list( manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance( ).getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) transform = self._scene.getRoot().callDecoration("getTransformMatrix") if transform and transform != Matrix(): transform_matrix = new_node.getLocalTransformation().preMultiply( transform.getInverse()) new_node.setTransformation(transform_matrix) front_offset = self._scene.getRoot().callDecoration( "getSceneFrontOffset") if global_container_stack.getProperty("blackbelt_raft", "value"): front_offset = front_offset - global_container_stack.getProperty("blackbelt_raft_margin", "value") \ - global_container_stack.getProperty("blackbelt_raft_thickness", "value") new_node.translate(Vector(0, 0, front_offset), SceneNode.TransformSpace.World) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def _createNodeFromObject(self, object, name=""): node = SceneNode() node.setName(name) mesh_builder = MeshBuilder() vertex_list = [] components = object.find(".//3mf:components", self._namespaces) if components: for component in components: id = component.get("objectid") new_object = self._root.find( "./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) new_node = self._createNodeFromObject( new_object, self._base_name + "_" + str(id)) node.addChild(new_node) transform = component.get("transform") if transform is not None: new_node.setTransformation( self._createMatrixFromTransformationString(transform)) # for vertex in entry.mesh.vertices.vertex: for vertex in object.findall(".//3mf:vertex", self._namespaces): vertex_list.append( [vertex.get("x"), vertex.get("y"), vertex.get("z")]) Job.yieldThread() xml_settings = list(object.findall(".//cura:setting", self._namespaces)) # Add the setting override decorator, so we can add settings to this node. if xml_settings: node.addDecorator(SettingOverrideDecorator()) global_container_stack = Application.getInstance( ).getGlobalContainerStack() # Ensure the correct next container for the SettingOverride decorator is set. if global_container_stack: multi_extrusion = global_container_stack.getProperty( "machine_extruder_count", "value") > 1 # Ensure that all extruder data is reset if not multi_extrusion: default_stack_id = global_container_stack.getId() else: default_stack = ExtruderManager.getInstance( ).getExtruderStack(0) if default_stack: default_stack_id = default_stack.getId() else: default_stack_id = global_container_stack.getId() node.callDecoration("setActiveExtruder", default_stack_id) # Get the definition & set it definition = QualityManager.getInstance( ).getParentMachineDefinition( global_container_stack.getBottom()) node.callDecoration("getStack").getTop().setDefinition( definition) setting_container = node.callDecoration("getStack").getTop() for setting in xml_settings: setting_key = setting.get("key") setting_value = setting.text # Extruder_nr is a special case. if setting_key == "extruder_nr": extruder_stack = ExtruderManager.getInstance( ).getExtruderStack(int(setting_value)) if extruder_stack: node.callDecoration("setActiveExtruder", extruder_stack.getId()) else: Logger.log("w", "Unable to find extruder in position %s", setting_value) continue setting_container.setProperty(setting_key, "value", setting_value) if len(node.getChildren()) > 0: group_decorator = GroupDecorator() node.addDecorator(group_decorator) triangles = object.findall(".//3mf:triangle", self._namespaces) mesh_builder.reserveFaceCount(len(triangles)) for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh_builder.addFaceByPoints( vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2], vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2], vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2]) Job.yieldThread() # TODO: We currently do not check for normals and simply recalculate them. mesh_builder.calculateNormals() mesh_builder.setFileName(name) mesh_data = mesh_builder.build() if len(mesh_data.getVertices()): node.setMeshData(mesh_data) node.setSelectable(True) return node
def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) object_id_map = {} new_node = SceneNode() ## Put all nodes in a dict identified by ID for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) else: object_id_map[id(node)] = node Job.yieldThread() settings = Application.getInstance().getMachineManager( ).getActiveProfile() center = None if not settings.getSettingValue("machine_center_is_zero"): center = numpy.array([ settings.getSettingValue("machine_width") / 2, 0.0, -settings.getSettingValue("machine_depth") / 2 ]) else: center = numpy.array([0.0, 0.0, 0.0]) mesh = MeshData() layer_data = LayerData.LayerData() layer_count = 0 for object in self._message.objects: layer_count += len(object.layers) current_layer = 0 for object in self._message.objects: try: node = object_id_map[object.id] except KeyError: continue for layer in object.layers: layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for polygon in layer.polygons: points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. points = numpy.asarray(points, dtype=numpy.float32) points /= 1000 points = numpy.insert(points, 1, (layer.height / 1000), axis=1) points[:, 2] *= -1 points -= center layer_data.addPolygon(layer.id, polygon.type, points, polygon.line_width) Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide()
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() global_container_stack = Application.getInstance( ).getGlobalContainerStack() machine_width = global_container_stack.getProperty( "machine_width", "value") machine_depth = global_container_stack.getProperty( "machine_depth", "value") arranger = Arrange.create(x=machine_width, y=machine_depth, 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 not_fit_count = 0 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). 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) 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, -not_fit_count * 20), set_position=True)) not_fit_count += 1 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" ), title=i18n_catalog.i18nc( "@info:title", "Can't Find Location")) no_full_solution_message.show() self.finished.emit(self)
def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]: Logger.log("d", "Preparing to load GCode") self._cancelled = False # We obtain the filament diameter from the selected extruder to calculate line widths global_stack = CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return None self._filament_diameter = global_stack.extruders[str( self._extruder_number)].getProperty("material_diameter", "value") scene_node = CuraSceneNode() gcode_list = [] self._is_layers_in_file = False self._extruder_offsets = self._extruderOffsets( ) # dict with index the extruder number. can be empty ############################################################################################## ## This part is where the action starts ############################################################################################## file_lines = 0 current_line = 0 for line in stream.split("\n"): file_lines += 1 gcode_list.append(line + "\n") if not self._is_layers_in_file and line[:len( self._layer_keyword)] == self._layer_keyword: self._is_layers_in_file = True file_step = max(math.floor(file_lines / 100), 1) self._clearValues() self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"), lifetime=0, title=catalog.i18nc("@info:title", "G-code Details")) assert (self._message is not None) # use for typing purposes self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing Gcode...") current_position = Position(0, 0, 0, 0, [0]) current_path = [] #type: List[List[float]] min_layer_number = 0 negative_layers = 0 previous_layer = 0 self._previous_extrusion_value = 0.0 for line in stream.split("\n"): if self._cancelled: Logger.log("d", "Parsing Gcode file cancelled") return None current_line += 1 if current_line % file_step == 0: self._message.setProgress( math.floor(current_line / file_lines * 100)) Job.yieldThread() if len(line) == 0: continue if line.find(self._type_keyword) == 0: type = line[len(self._type_keyword):].strip() if type == "WALL-INNER": self._layer_type = LayerPolygon.InsetXType elif type == "WALL-OUTER": self._layer_type = LayerPolygon.Inset0Type elif type == "SKIN": self._layer_type = LayerPolygon.SkinType elif type == "SKIRT": self._layer_type = LayerPolygon.SkirtType elif type == "SUPPORT": self._layer_type = LayerPolygon.SupportType elif type == "FILL": self._layer_type = LayerPolygon.InfillType elif type == "SUPPORT-INTERFACE": self._layer_type = LayerPolygon.SupportInterfaceType elif type == "PRIME-TOWER": self._layer_type = LayerPolygon.PrimeTowerType else: Logger.log( "w", "Encountered a unknown type (%s) while parsing g-code.", type) # When the layer change is reached, the polygon is computed so we have just one layer per extruder if self._is_layers_in_file and line[:len(self._layer_keyword )] == self._layer_keyword: try: layer_number = int(line[len(self._layer_keyword):]) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # Start the new layer at the end position of the last layer current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob if layer_number < min_layer_number: min_layer_number = layer_number if layer_number < 0: layer_number += abs(min_layer_number) negative_layers += 1 else: layer_number += negative_layers # In case there is a gap in the layer count, empty layers are created for empty_layer in range(previous_layer + 1, layer_number): self._createEmptyLayer(empty_layer) self._layer_number = layer_number previous_layer = layer_number except: pass # This line is a comment. Ignore it (except for the layer_keyword) if line.startswith(";"): continue G = self._getInt(line, "G") if G is not None: # When find a movement, the new posistion is calculated and added to the current_path, but # don't need to create a polygon until the end of the layer current_position = self.processGCode(G, line, current_position, current_path) continue # When changing the extruder, the polygon with the stored paths is computed if line.startswith("T"): T = self._getInt(line, "T") if T is not None: self._extruders_seen.add(T) self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # When changing tool, store the end point of the previous path, then process the code and finally # add another point with the new position of the head. current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) current_position = self.processTCode( T, line, current_position, current_path) current_path.append([ current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType ]) if line.startswith("M"): M = self._getInt(line, "M") if M is not None: self.processMCode(M, line, current_position, current_path) # "Flush" leftovers. Last layer paths are still stored if len(current_path) > 1: if self._createPolygon( self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() material_color_map = numpy.zeros((8, 4), dtype=numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0] material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0] material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0] material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0] material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0] material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) gcode_list_decorator = GCodeListDecorator() gcode_list_decorator.setGcodeFileName(filename) gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) # gcode_dict stores gcode_lists for a number of build plates. active_build_plate_id = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_dict = {active_build_plate_id: gcode_list} CuraApplication.getInstance().getController().getScene( ).gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically. Logger.log("d", "Finished parsing Gcode") self._message.hide() if self._layer_number == 0: Logger.log("w", "File doesn't contain any valid layers") if not global_stack.getProperty("machine_center_is_zero", "value"): machine_width = global_stack.getProperty("machine_width", "value") machine_depth = global_stack.getProperty("machine_depth", "value") scene_node.setPosition( Vector(-machine_width / 2, 0, machine_depth / 2)) Logger.log("d", "GCode loading finished") if CuraApplication.getInstance().getPreferences().getValue( "gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate." ), lifetime=0, title=catalog.i18nc( "@info:title", "G-code Details")) caution_message.show() # The "save/print" button's state is bound to the backend state. backend = CuraApplication.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node
def read(self, file_name): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [ name for name in archive.namelist() if name.startswith("Cura/") ] # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its # parsing code. temp_preferences = Preferences() temp_preferences.readFromFile( io.TextIOWrapper(archive.open("Cura/preferences.cfg")) ) # We need to wrap it, else the archive parser breaks. # Copy a number of settings from the temp preferences to the global global_preferences = Preferences.getInstance() visible_settings = temp_preferences.getValue( "general/visible_settings") if visible_settings is None: Logger.log( "w", "Workspace did not contain visible settings. Leaving visibility unchanged" ) else: global_preferences.setValue("general/visible_settings", visible_settings) categories_expanded = temp_preferences.getValue( "cura/categories_expanded") if categories_expanded is None: Logger.log( "w", "Workspace did not contain expanded categories. Leaving them unchanged" ) else: global_preferences.setValue("cura/categories_expanded", categories_expanded) Application.getInstance().expandedCategoriesChanged.emit( ) # Notify the GUI of the change self._id_mapping = {} # We don't add containers right away, but wait right until right before the stack serialization. # We do this so that if something goes wrong, it's easier to clean up. containers_to_add = [] # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. Logger.log("d", "Workspace loading is checking definitions...") # Get all the definition files & check if they exist. If not, add them. definition_container_files = [ name for name in cura_file_names if name.endswith(self._definition_container_suffix) ] for definition_container_file in definition_container_files: container_id = self._stripFileToId(definition_container_file) definitions = self._container_registry.findDefinitionContainers( id=container_id) if not definitions: definition_container = DefinitionContainer(container_id) definition_container.deserialize( archive.open(definition_container_file).read().decode( "utf-8")) self._container_registry.addContainer(definition_container) Job.yieldThread() Logger.log("d", "Workspace loading is checking materials...") material_containers = [] # Get all the material files and check if they exist. If not, add them. xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer( xml_material_profile).suffixes[0] if xml_material_profile: material_container_files = [ name for name in cura_file_names if name.endswith(self._material_container_suffix) ] for material_container_file in material_container_files: container_id = self._stripFileToId(material_container_file) materials = self._container_registry.findInstanceContainers( id=container_id) if not materials: material_container = xml_material_profile(container_id) material_container.deserialize( archive.open(material_container_file).read().decode( "utf-8")) containers_to_add.append(material_container) else: if not materials[0].isReadOnly( ): # Only create new materials if they are not read only. if self._resolve_strategies["material"] == "override": materials[0].deserialize( archive.open(material_container_file).read(). decode("utf-8")) elif self._resolve_strategies["material"] == "new": # Note that we *must* deserialize it with a new ID, as multiple containers will be # auto created & added. material_container = xml_material_profile( self.getNewId(container_id)) material_container.deserialize( archive.open(material_container_file).read(). decode("utf-8")) containers_to_add.append(material_container) material_containers.append(material_container) Job.yieldThread() Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace instance_container_files = [ name for name in cura_file_names if name.endswith(self._instance_container_suffix) ] user_instance_containers = [] quality_changes_instance_containers = [] for instance_container_file in instance_container_files: container_id = self._stripFileToId(instance_container_file) instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize( archive.open(instance_container_file).read().decode("utf-8")) container_type = instance_container.getMetaDataEntry("type") Job.yieldThread() if container_type == "user": # Check if quality changes already exists. user_containers = self._container_registry.findInstanceContainers( id=container_id) if not user_containers: containers_to_add.append(instance_container) else: if self._resolve_strategies[ "machine"] == "override" or self._resolve_strategies[ "machine"] is None: user_containers[0].deserialize( archive.open(instance_container_file).read(). decode("utf-8")) elif self._resolve_strategies["machine"] == "new": # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. extruder_id = instance_container.getMetaDataEntry( "extruder", None) if extruder_id: new_id = self.getNewId( extruder_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry( "extruder", self.getNewId(extruder_id)) containers_to_add.append(instance_container) machine_id = instance_container.getMetaDataEntry( "machine", None) if machine_id: new_id = self.getNewId( machine_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry( "machine", self.getNewId(machine_id)) containers_to_add.append(instance_container) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers( id=container_id) if not quality_changes: containers_to_add.append(instance_container) else: if self._resolve_strategies[ "quality_changes"] == "override": quality_changes[0].deserialize( archive.open(instance_container_file).read(). decode("utf-8")) elif self._resolve_strategies["quality_changes"] is None: # The ID already exists, but nothing in the values changed, so do nothing. pass quality_changes_instance_containers.append(instance_container) else: continue # Add all the containers right before we try to add / serialize the stack for container in containers_to_add: self._container_registry.addContainer(container) container.setDirty(True) # Get the stack(s) saved in the workspace. Logger.log("d", "Workspace loading is checking stacks containers...") container_stack_files = [ name for name in cura_file_names if name.endswith(self._container_stack_suffix) ] global_stack = None extruder_stacks = [] container_stacks_added = [] try: for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks( id=container_id) if container_stacks: stack = container_stacks[0] if self._resolve_strategies["machine"] == "override": # TODO: HACK # There is a machine, check if it has authenticationd data. If so, keep that data. network_authentication_id = container_stacks[ 0].getMetaDataEntry("network_authentication_id") network_authentication_key = container_stacks[ 0].getMetaDataEntry("network_authentication_key") container_stacks[0].deserialize( archive.open(container_stack_file).read().decode( "utf-8")) if network_authentication_id: container_stacks[0].addMetaDataEntry( "network_authentication_id", network_authentication_id) if network_authentication_key: container_stacks[0].addMetaDataEntry( "network_authentication_key", network_authentication_key) elif self._resolve_strategies["machine"] == "new": new_id = self.getNewId(container_id) stack = ContainerStack(new_id) stack.deserialize( archive.open(container_stack_file).read().decode( "utf-8")) # Ensure a unique ID and name stack._id = new_id # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the # bound machine also needs to change. if stack.getMetaDataEntry("machine", None): stack.setMetaDataEntry( "machine", self.getNewId( stack.getMetaDataEntry("machine"))) if stack.getMetaDataEntry("type") != "extruder_train": # Only machines need a new name, stacks may be non-unique stack.setName( self._container_registry.uniqueName( stack.getName())) container_stacks_added.append(stack) self._container_registry.addContainer(stack) else: Logger.log( "w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize( archive.open(container_stack_file).read().decode( "utf-8")) container_stacks_added.append(stack) self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: global_stack = stack Job.yieldThread() except: Logger.logException( "w", "We failed to serialize the stack. Trying to clean up.") # Something went really wrong. Try to remove any data that we added. for container in containers_to_add: self._container_registry.getInstance().removeContainer( container.getId()) for container in container_stacks_added: self._container_registry.getInstance().removeContainer( container.getId()) return None if self._resolve_strategies["machine"] == "new": # A new machine was made, but it was serialized with the wrong user container. Fix that now. for container in user_instance_containers: extruder_id = container.getMetaDataEntry("extruder", None) if extruder_id: for extruder in extruder_stacks: if extruder.getId() == extruder_id: extruder.replaceContainer(0, container) continue machine_id = container.getMetaDataEntry("machine", None) if machine_id: if global_stack.getId() == machine_id: global_stack.replaceContainer(0, container) continue if self._resolve_strategies["quality_changes"] == "new": # Quality changes needs to get a new ID, added to registry and to the right stacks for container in quality_changes_instance_containers: old_id = container.getId() container.setName( self._container_registry.uniqueName(container.getName())) # We're not really supposed to change the ID in normal cases, but this is an exception. container._id = self.getNewId(container.getId()) # The container was not added yet, as it didn't have an unique ID. It does now, so add it. self._container_registry.addContainer(container) # Replace the quality changes container old_container = global_stack.findContainer( {"type": "quality_changes"}) if old_container.getId() == old_id: quality_changes_index = global_stack.getContainerIndex( old_container) global_stack.replaceContainer(quality_changes_index, container) continue for stack in extruder_stacks: old_container = stack.findContainer( {"type": "quality_changes"}) if old_container.getId() == old_id: quality_changes_index = stack.getContainerIndex( old_container) stack.replaceContainer(quality_changes_index, container) if self._resolve_strategies["material"] == "new": for material in material_containers: old_material = global_stack.findContainer({"type": "material"}) if old_material.getId() in self._id_mapping: material_index = global_stack.getContainerIndex( old_material) global_stack.replaceContainer(material_index, material) continue for stack in extruder_stacks: old_material = stack.findContainer({"type": "material"}) if old_material.getId() in self._id_mapping: material_index = stack.getContainerIndex(old_material) stack.replaceContainer(material_index, material) continue for stack in extruder_stacks: ExtruderManager.getInstance().registerExtruder( stack, global_stack.getId()) else: # Machine has no extruders, but it needs to be registered with the extruder manager. ExtruderManager.getInstance().registerExtruder( None, global_stack.getId()) Logger.log( "d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. global_stack.containersChanged.emit(global_stack.getTop()) for stack in extruder_stacks: stack.setNextStack(global_stack) stack.containersChanged.emit(stack.getTop()) # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) # Load all the nodes / meshdata of the workspace nodes = self._3mf_mesh_reader.read(file_name) if nodes is None: nodes = [] return nodes