Пример #1
0
    def _removeEraserMesh(self, node: SteSlicerSceneNode):
        parent = node.getParent()
        if parent == self._controller.getScene().getRoot():
            parent = None

        op = RemoveSceneNodeOperation(node)
        op.push()

        if parent and not Selection.isSelected(parent):
            Selection.add(parent)

        SteSlicerApplication.getInstance().getController().getScene(
        ).sceneChanged.emit(node)
Пример #2
0
    def _handlePerObjectSettings(self, node: SteSlicerSceneNode,
                                 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 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 = SteSlicerSceneNode(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 isinstance(layer, Arcus.PythonMessage):
                if layer.repeatedMessageCount("path_segment") > 0:
                    if layer.id < min_layer_number:
                        min_layer_number = layer.id
                    if layer.id < 0:
                        negative_layers += 1
            else:
                if layer.id < min_layer_number:
                    min_layer_number = layer.id
                if layer.id < 0:
                    negative_layers += 1

        current_layer = 0
        additional_first_layers = 0
        for layer in self._layers:
            if isinstance(layer, Layer):
                layer_data.addLayer(current_layer, layer)
                additional_first_layers += 1
            else:

                # 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
                abs_layer_number += additional_first_layers
                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.
                    elif polygon.point_type == 1:  # Point3D
                        points = points.reshape((-1, 3))
                    else:  # Point6D
                        points = points.reshape((-1, 6))

                    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.
                    if polygon.point_type in [0, 1]:
                        new_points = numpy.empty((len(points), 3),
                                                 numpy.float32)
                    else:
                        new_points = numpy.empty((len(points), 6),
                                                 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]
                    elif polygon.point_type == 1:  # Point3D
                        new_points[:, 0] = points[:, 0]
                        new_points[:, 1] = points[:, 2]
                        new_points[:, 2] = -points[:, 1]
                    else:
                        new_points[:, 0] = points[:, 0]
                        new_points[:, 1] = points[:, 2]
                        new_points[:, 2] = -points[:, 1]
                        new_points[:, 3] = points[:, 3]
                        new_points[:, 4] = points[:, 5]
                        new_points[:, 5] = -points[:, 4]

                    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()
            contains_arcus_layers = False
            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"))  # 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(
                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)
Пример #4
0
    def _createEraserMesh(self, parent: SteSlicerSceneNode, position: Vector):
        node = SteSlicerSceneNode()

        node.setName("Eraser")
        node.setSelectable(True)
        mesh = self._createCube(10)
        node.setMeshData(mesh.build())

        active_build_plate = SteSlicerApplication.getInstance(
        ).getMultiBuildPlateModel().activeBuildPlate
        node.addDecorator(BuildPlateDecorator(active_build_plate))
        node.addDecorator(SliceableObjectDecorator())

        stack = node.callDecoration(
            "getStack"
        )  # created by SettingOverrideDecorator that is automatically added to SteSlicerSceneNode
        settings = stack.getTop()

        definition = stack.getSettingDefinition("anti_overhang_mesh")
        new_instance = SettingInstance(definition, settings)
        new_instance.setProperty("value", True)
        new_instance.resetState(
        )  # Ensure that the state is not seen as a user state.
        settings.addInstance(new_instance)

        op = GroupedOperation()
        # First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent
        op.addOperation(
            AddSceneNodeOperation(node,
                                  self._controller.getScene().getRoot()))
        op.addOperation(SetParentOperation(node, parent))
        op.push()
        node.setPosition(position, SteSlicerSceneNode.TransformSpace.World)

        SteSlicerApplication.getInstance().getController().getScene(
        ).sceneChanged.emit(node)
Пример #5
0
    def _generateSceneNode(self, file_name, xz_size, peak_height, base_height,
                           blur_iterations, max_size, image_color_invert):
        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)

        base_height = max(base_height, 0)
        peak_height = max(peak_height, -base_height)

        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)
                avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 *
                                                                        255)
                height_data[y, x] = avg

        Job.yieldThread()

        if image_color_invert:
            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()

        height_data *= scale_vector.y
        height_data += base_height

        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
Пример #6
0
    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
Пример #7
0
    def processGCodeStream(self, stream: str) -> Optional[SteSlicerSceneNode]:
        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 = SteSlicerApplication.getInstance().getGlobalContainerStack()

        if not global_stack:
            return None

        self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")

        scene_node = SteSlicerSceneNode()

        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, 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
                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.a, current_position.b, current_position.c, 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._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.a, current_position.b, current_position.c, 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.a, current_position.b, current_position.c, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])

            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((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}
        SteSlicerApplication.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")

        settings = SteSlicerApplication.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            machine_width = settings.getProperty("machine_width", "value")
            machine_depth = settings.getProperty("machine_depth", "value")
            scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "GCode 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()

        # The "save/print" button's state is bound to the backend state.
        backend = SteSlicerApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
Пример #8
0
    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)
Пример #9
0
    def _convertSavitarNodeToUMNode(self, savitar_node):
        self._object_count += 1
        node_name = "Object %s" % self._object_count

        active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate

        um_node = SteSlicerSceneNode() # This adds a SettingOverrideDecorator
        um_node.addDecorator(BuildPlateDecorator(active_build_plate))
        um_node.setName(node_name)
        transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
        um_node.setTransformation(transformation)
        mesh_builder = MeshBuilder()

        data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)

        vertices = numpy.resize(data, (int(data.size / 3), 3))
        mesh_builder.setVertices(vertices)
        mesh_builder.calculateNormals(fast=True)
        mesh_data = mesh_builder.build()

        if len(mesh_data.getVertices()):
            um_node.setMeshData(mesh_data)

        for child in savitar_node.getChildren():
            child_node = self._convertSavitarNodeToUMNode(child)
            if child_node:
                um_node.addChild(child_node)

        if um_node.getMeshData() is None and len(um_node.getChildren()) == 0:
            return None

        settings = savitar_node.getSettings()

        # Add the setting override decorator, so we can add settings to this node.
        if settings:
            global_container_stack = Application.getInstance().getGlobalContainerStack()

            # Ensure the correct next container for the SettingOverride decorator is set.
            if global_container_stack:
                default_stack = ExtruderManager.getInstance().getExtruderStack(0)

                if default_stack:
                    um_node.callDecoration("setActiveExtruder", default_stack.getId())

                # Get the definition & set it
                definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
                um_node.callDecoration("getStack").getTop().setDefinition(definition_id)

            setting_container = um_node.callDecoration("getStack").getTop()

            for key in settings:
                setting_value = settings[key]

                # Extruder_nr is a special case.
                if key == "extruder_nr":
                    extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
                    if extruder_stack:
                        um_node.callDecoration("setActiveExtruder", extruder_stack.getId())
                    else:
                        Logger.log("w", "Unable to find extruder in position %s", setting_value)
                    continue
                setting_container.setProperty(key, "value", setting_value)

        if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None:
            group_decorator = GroupDecorator()
            um_node.addDecorator(group_decorator)
        um_node.setSelectable(True)
        if um_node.getMeshData():
            # Assuming that all nodes with mesh data are printable objects
            # affects (auto) slicing
            sliceable_decorator = SliceableObjectDecorator()
            um_node.addDecorator(sliceable_decorator)
        return um_node