예제 #1
0
 def test_multiply(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10,10,10))
     temp_matrix2 = Matrix()
     temp_matrix2.setByScaleFactor(0.5)
     temp_matrix.multiply(temp_matrix2)
     numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,10],[0,0.5,0,10],[0,0,0.5,10],[0,0,0,1]]))
예제 #2
0
 def test_multiply(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10,10,10))
     temp_matrix2 = Matrix()
     temp_matrix2.setByScaleFactor(0.5)
     temp_matrix.multiply(temp_matrix2)
     numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,10],[0,0.5,0,10],[0,0,0.5,10],[0,0,0,1]]))
예제 #3
0
 def test_multiplyCopy(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10, 10, 10))
     temp_matrix2 = Matrix()
     temp_matrix2.setByScaleFactor(0.5)
     result = temp_matrix.multiply(temp_matrix2, copy=True)
     assert temp_matrix != result
     numpy.testing.assert_array_almost_equal(
         result.getData(),
         numpy.array([[0.5, 0, 0, 10], [0, 0.5, 0, 10], [0, 0, 0.5, 10],
                      [0, 0, 0, 1]]))
예제 #4
0
    def read(self, file_name):
        result = []
        # The base object of 3mf is a zipped archive.
        try:
            archive = zipfile.ZipFile(file_name, "r")
            self._base_name = os.path.basename(file_name)
            parser = Savitar.ThreeMFParser()
            scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
            self._unit = scene_3mf.getUnit()
            for node in scene_3mf.getSceneNodes():
                um_node = self._convertSavitarNodeToUMNode(node)
                if um_node is None:
                    continue
                # compensate for original center position, if object(s) is/are not around its zero position

                transform_matrix = Matrix()
                mesh_data = um_node.getMeshData()
                if mesh_data is not None:
                    extents = mesh_data.getExtents()
                    center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
                    transform_matrix.setByTranslation(center_vector)
                transform_matrix.multiply(um_node.getLocalTransformation())
                um_node.setTransformation(transform_matrix)

                global_container_stack = Application.getInstance().getGlobalContainerStack()

                # Create a transformation Matrix to convert from 3mf worldspace into ours.
                # First step: flip the y and z axis.
                transformation_matrix = Matrix()
                transformation_matrix._data[1, 1] = 0
                transformation_matrix._data[1, 2] = 1
                transformation_matrix._data[2, 1] = -1
                transformation_matrix._data[2, 2] = 0

                # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
                # build volume.
                if global_container_stack:
                    translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2,
                                                y=-global_container_stack.getProperty("machine_depth", "value") / 2,
                                                z=0)
                    translation_matrix = Matrix()
                    translation_matrix.setByTranslation(translation_vector)
                    transformation_matrix.multiply(translation_matrix)

                # Third step: 3MF also defines a unit, wheras Cura always assumes mm.
                scale_matrix = Matrix()
                scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
                transformation_matrix.multiply(scale_matrix)

                # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
                um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))

                result.append(um_node)

        except Exception:
            Logger.logException("e", "An exception occurred in 3mf reader.")
            return []

        return result
예제 #5
0
    def render(self, renderer):
        if not self._shader:
            # We now misuse the platform shader, as it actually supports textures
            self._shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "platform.shader"))
            # Set the opacity to 0, so that the template is in full control.
            self._shader.setUniformValue("u_opacity", 0)
            self._texture = OpenGL.getInstance().createTexture()
            document = QTextDocument()
            document.setHtml(
                self._getFilledTemplate(self._display_data, self._template))

            texture_image = QImage(self._texture_width, self._texture_height,
                                   QImage.Format_ARGB32)
            texture_image.fill(Qt.transparent)
            painter = QPainter(texture_image)
            document.drawContents(
                painter,
                QRectF(0., 0., self._texture_width, self._texture_height))
            painter.end()
            self._texture.setImage(texture_image)
            self._shader.setTexture(0, self._texture)

        node_position = self._target_node.getWorldPosition()
        position_matrix = Matrix()
        position_matrix.setByTranslation(node_position)
        camera_orientation = self._scene.getActiveCamera().getOrientation(
        ).toMatrix()

        renderer.queueNode(self._scene.getRoot(),
                           shader=self._shader,
                           transparent=True,
                           mesh=self._billboard_mesh.getTransformed(
                               position_matrix.multiply(camera_orientation)),
                           sort=self._target_node.getDepth())

        return True  # This node does it's own rendering.
예제 #6
0
파일: Camera.py 프로젝트: greatnam/Uranium
class Camera(SceneNode.SceneNode):
    class PerspectiveMode(enum.Enum):
        PERSPECTIVE = "perspective"
        ORTHOGRAPHIC = "orthographic"

    @staticmethod
    def getDefaultZoomFactor() -> float:
        return -0.3334

    def __init__(self,
                 name: str = "",
                 parent: Optional[SceneNode.SceneNode] = None) -> None:
        super().__init__(parent)
        self._name = name  # type: str
        self._projection_matrix = Matrix()  # type: Matrix
        self._projection_matrix.setOrtho(-5, 5, -5, 5, -100, 100)
        self._perspective = True  # type: bool
        self._viewport_width = 0  # type: int
        self._viewport_height = 0  # type: int
        self._window_width = 0  # type: int
        self._window_height = 0  # type: int
        self._auto_adjust_view_port_size = True  # type: bool
        self.setCalculateBoundingBox(False)
        self._cached_view_projection_matrix = None  # type: Optional[Matrix]

        self._zoom_factor = Camera.getDefaultZoomFactor()

        from UM.Application import Application
        Application.getInstance().getPreferences().addPreference(
            "general/camera_perspective_mode",
            default_value=self.PerspectiveMode.PERSPECTIVE.value)
        Application.getInstance().getPreferences().preferenceChanged.connect(
            self._preferencesChanged)
        self._preferencesChanged("general/camera_perspective_mode")

    def __deepcopy__(self, memo: Dict[int, object]) -> "Camera":
        copy = cast(Camera, super().__deepcopy__(memo))
        copy._projection_matrix = self._projection_matrix
        copy._window_height = self._window_height
        copy._window_width = self._window_width
        copy._viewport_height = self._viewport_height
        copy._viewport_width = self._viewport_width
        return copy

    def getZoomFactor(self):
        return self._zoom_factor

    def setZoomFactor(self, zoom_factor: float) -> None:
        # Only an orthographic camera has a zoom at the moment.
        if not self.isPerspective():
            if self._zoom_factor != zoom_factor:
                self._zoom_factor = zoom_factor
                self._updatePerspectiveMatrix()

    def setMeshData(self, mesh_data: Optional["MeshData"]) -> None:
        assert mesh_data is None, "Camera's can't have mesh data"

    def getAutoAdjustViewPort(self) -> bool:
        return self._auto_adjust_view_port_size

    def setAutoAdjustViewPort(self, auto_adjust: bool) -> None:
        self._auto_adjust_view_port_size = auto_adjust

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self) -> Matrix:
        return self._projection_matrix

    def getViewportWidth(self) -> int:
        return self._viewport_width

    def setViewportWidth(self, width: int) -> None:
        self._viewport_width = width
        self._updatePerspectiveMatrix()

    def setViewportHeight(self, height: int) -> None:
        self._viewport_height = height
        self._updatePerspectiveMatrix()

    def setViewportSize(self, width: int, height: int) -> None:
        self._viewport_width = width
        self._viewport_height = height
        self._updatePerspectiveMatrix()

    def _updatePerspectiveMatrix(self) -> None:
        view_width = self._viewport_width
        view_height = self._viewport_height
        projection_matrix = Matrix()
        if self.isPerspective():
            if view_width != 0 and view_height != 0:
                projection_matrix.setPerspective(30, view_width / view_height,
                                                 1, 500)
        else:
            # Almost no near/far plane, please.
            if view_width != 0 and view_height != 0:
                horizontal_zoom = view_width * self._zoom_factor
                vertical_zoom = view_height * self._zoom_factor
                projection_matrix.setOrtho(-view_width / 2 - horizontal_zoom,
                                           view_width / 2 + horizontal_zoom,
                                           -view_height / 2 - vertical_zoom,
                                           view_height / 2 + vertical_zoom,
                                           -9001, 9001)
        self.setProjectionMatrix(projection_matrix)
        self.perspectiveChanged.emit(self)

    def getViewProjectionMatrix(self) -> Matrix:
        if self._cached_view_projection_matrix is None:
            inverted_transformation = self.getWorldTransformation()
            inverted_transformation.invert()
            self._cached_view_projection_matrix = self._projection_matrix.multiply(
                inverted_transformation, copy=True)
        return self._cached_view_projection_matrix

    def _updateWorldTransformation(self) -> None:
        self._cached_view_projection_matrix = None
        super()._updateWorldTransformation()

    def getViewportHeight(self) -> int:
        return self._viewport_height

    def setWindowSize(self, width: int, height: int) -> None:
        self._window_width = width
        self._window_height = height

    def getWindowSize(self) -> Tuple[int, int]:
        return self._window_width, self._window_height

    def render(self, renderer) -> bool:
        # It's a camera. It doesn't need rendering.
        return True

    ##  Set the projection matrix of this camera.
    #   \param matrix The projection matrix to use for this camera.
    def setProjectionMatrix(self, matrix: Matrix) -> None:
        self._projection_matrix = matrix
        self._cached_view_projection_matrix = None

    def isPerspective(self) -> bool:
        return self._perspective

    def setPerspective(self, perspective: bool) -> None:
        if self._perspective != perspective:
            self._perspective = perspective
            self._updatePerspectiveMatrix()

    perspectiveChanged = Signal()

    ##  Get a ray from the camera into the world.
    #
    #   This will create a ray from the camera's origin, passing through (x, y)
    #   on the near plane and continuing based on the projection matrix.
    #
    #   \param x The X coordinate on the near plane this ray should pass through.
    #   \param y The Y coordinate on the near plane this ray should pass through.
    #
    #   \return A Ray object representing a ray from the camera origin through X, Y.
    #
    #   \note The near-plane coordinates should be in normalized form, that is within (-1, 1).
    def getRay(self, x: float, y: float) -> Ray:
        window_x = ((x + 1) / 2) * self._window_width
        window_y = ((y + 1) / 2) * self._window_height
        view_x = (window_x / self._viewport_width) * 2 - 1
        view_y = (window_y / self._viewport_height) * 2 - 1

        inverted_projection = numpy.linalg.inv(
            self._projection_matrix.getData().copy())
        transformation = self.getWorldTransformation().getData()

        near = numpy.array([view_x, -view_y, -1.0, 1.0], dtype=numpy.float32)
        near = numpy.dot(inverted_projection, near)
        near = numpy.dot(transformation, near)
        near = near[0:3] / near[3]

        far = numpy.array([view_x, -view_y, 1.0, 1.0], dtype=numpy.float32)
        far = numpy.dot(inverted_projection, far)
        far = numpy.dot(transformation, far)
        far = far[0:3] / far[3]

        direction = far - near
        direction /= numpy.linalg.norm(direction)

        if self.isPerspective():
            origin = self.getWorldPosition()
            direction = -direction
        else:
            # In orthographic mode, the origin is the click position on the plane where the camera resides, and that
            # plane is parallel to the near and the far planes.
            projection = numpy.array([view_x, -view_y, 0.0, 1.0],
                                     dtype=numpy.float32)
            projection = numpy.dot(inverted_projection, projection)
            projection = numpy.dot(transformation, projection)
            projection = projection[0:3] / projection[3]

            origin = Vector(data=projection)

        return Ray(origin, Vector(direction[0], direction[1], direction[2]))

    ##  Project a 3D position onto the 2D view plane.
    def project(self, position: Vector) -> Tuple[float, float]:
        projection = self._projection_matrix
        view = self.getWorldTransformation()
        view.invert()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)
        return position.x / position.z / 2.0, position.y / position.z / 2.0

    ##  Updates the _perspective field if the preference was modified.
    def _preferencesChanged(self, key: str) -> None:
        if key != "general/camera_perspective_mode":  # Only listen to camera_perspective_mode.
            return
        from UM.Application import Application
        new_mode = str(Application.getInstance().getPreferences().getValue(
            "general/camera_perspective_mode"))

        # Translate the selected mode to the camera state.
        if new_mode == str(self.PerspectiveMode.ORTHOGRAPHIC.value):
            Logger.log("d", "Changing perspective mode to orthographic.")
            self.setPerspective(False)
        elif new_mode == str(self.PerspectiveMode.PERSPECTIVE.value):
            Logger.log("d", "Changing perspective mode to perspective.")
            self.setPerspective(True)
        else:
            Logger.log(
                "w", "Unknown perspective mode {new_mode}".format(
                    new_mode=new_mode))
예제 #7
0
    def read(self, file_name):
        result = []
        # The base object of 3mf is a zipped archive.
        archive = zipfile.ZipFile(file_name, "r")
        self._base_name = os.path.basename(file_name)
        try:
            self._root = ET.parse(archive.open("3D/3dmodel.model"))
            self._unit = self._root.getroot().get("unit")

            build_items = self._root.findall("./3mf:build/3mf:item",
                                             self._namespaces)

            for build_item in build_items:
                id = build_item.get("objectid")
                object = self._root.find(
                    "./3mf:resources/3mf:object[@id='{0}']".format(id),
                    self._namespaces)
                if "type" in object.attrib:
                    if object.attrib["type"] == "support" or object.attrib[
                            "type"] == "other":
                        # Ignore support objects, as cura does not support these.
                        # We can't guarantee that they wont be made solid.
                        # We also ignore "other", as I have no idea what to do with them.
                        Logger.log(
                            "w",
                            "3MF file contained an object of type %s which is not supported by Cura",
                            object.attrib["type"])
                        continue
                    elif object.attrib[
                            "type"] == "solidsupport" or object.attrib[
                                "type"] == "model":
                        pass  # Load these as normal
                    else:
                        # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway.
                        Logger.log(
                            "e",
                            "3MF file contained an object of type %s which is not supported by the 3mf spec",
                            object.attrib["type"])
                        continue

                build_item_node = self._createNodeFromObject(
                    object, self._base_name + "_" + str(id))

                # compensate for original center position, if object(s) is/are not around its zero position
                transform_matrix = Matrix()
                mesh_data = build_item_node.getMeshData()
                if mesh_data is not None:
                    extents = mesh_data.getExtents()
                    center_vector = Vector(extents.center.x, extents.center.y,
                                           extents.center.z)
                    transform_matrix.setByTranslation(center_vector)

                # offset with transform from 3mf
                transform = build_item.get("transform")
                if transform is not None:
                    transform_matrix.multiply(
                        self._createMatrixFromTransformationString(transform))

                build_item_node.setTransformation(transform_matrix)

                global_container_stack = UM.Application.getInstance(
                ).getGlobalContainerStack()

                # Create a transformation Matrix to convert from 3mf worldspace into ours.
                # First step: flip the y and z axis.
                transformation_matrix = Matrix()
                transformation_matrix._data[1, 1] = 0
                transformation_matrix._data[1, 2] = 1
                transformation_matrix._data[2, 1] = -1
                transformation_matrix._data[2, 2] = 0

                # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
                # build volume.
                if global_container_stack:
                    translation_vector = Vector(
                        x=-global_container_stack.getProperty(
                            "machine_width", "value") / 2,
                        y=-global_container_stack.getProperty(
                            "machine_depth", "value") / 2,
                        z=0)
                    translation_matrix = Matrix()
                    translation_matrix.setByTranslation(translation_vector)
                    transformation_matrix.multiply(translation_matrix)

                # Third step: 3MF also defines a unit, wheras Cura always assumes mm.
                scale_matrix = Matrix()
                scale_matrix.setByScaleVector(
                    self._getScaleFromUnit(self._unit))
                transformation_matrix.multiply(scale_matrix)

                # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
                build_item_node.setTransformation(
                    build_item_node.getLocalTransformation().preMultiply(
                        transformation_matrix))

                result.append(build_item_node)

        except Exception as e:
            Logger.log("e", "An exception occurred in 3mf reader: %s", e)

        return result
예제 #8
0
    def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
        result = []
        self._object_count = 0  # Used to name objects as there is no node name yet.
        # The base object of 3mf is a zipped archive.
        try:
            archive = zipfile.ZipFile(file_name, "r")
            self._base_name = os.path.basename(file_name)
            parser = Savitar.ThreeMFParser()
            scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
            self._unit = scene_3mf.getUnit()
            for node in scene_3mf.getSceneNodes():
                um_node = self._convertSavitarNodeToUMNode(node)
                if um_node is None:
                    continue
                # compensate for original center position, if object(s) is/are not around its zero position

                transform_matrix = Matrix()
                mesh_data = um_node.getMeshData()
                if mesh_data is not None:
                    extents = mesh_data.getExtents()
                    if extents is not None:
                        center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
                        transform_matrix.setByTranslation(center_vector)
                transform_matrix.multiply(um_node.getLocalTransformation())
                um_node.setTransformation(transform_matrix)

                global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()

                # Create a transformation Matrix to convert from 3mf worldspace into ours.
                # First step: flip the y and z axis.
                transformation_matrix = Matrix()
                transformation_matrix._data[1, 1] = 0
                transformation_matrix._data[1, 2] = 1
                transformation_matrix._data[2, 1] = -1
                transformation_matrix._data[2, 2] = 0

                # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
                # build volume.
                if global_container_stack:
                    translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
                                                y = -global_container_stack.getProperty("machine_depth", "value") / 2,
                                                z = 0)
                    translation_matrix = Matrix()
                    translation_matrix.setByTranslation(translation_vector)
                    transformation_matrix.multiply(translation_matrix)

                # Third step: 3MF also defines a unit, whereas Cura always assumes mm.
                scale_matrix = Matrix()
                scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
                transformation_matrix.multiply(scale_matrix)

                # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
                um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))

                # Check if the model is positioned below the build plate and honor that when loading project files.
                node_meshdata = um_node.getMeshData()
                if node_meshdata is not None:
                    aabb = node_meshdata.getExtents(um_node.getWorldTransformation())
                    if aabb is not None:
                        minimum_z_value = aabb.minimum.y  # y is z in transformation coordinates
                        if minimum_z_value < 0:
                            um_node.addDecorator(ZOffsetDecorator())
                            um_node.callDecoration("setZOffset", minimum_z_value)

                result.append(um_node)

        except Exception:
            Logger.logException("e", "An exception occurred in 3mf reader.")
            return []

        return result
예제 #9
0
파일: ThreeMFReader.py 프로젝트: mifga/Cura
    def read(self, file_name):
        result = []
        # The base object of 3mf is a zipped archive.
        archive = zipfile.ZipFile(file_name, "r")
        self._base_name = os.path.basename(file_name)
        try:
            self._root = ET.parse(archive.open("3D/3dmodel.model"))
            self._unit = self._root.getroot().get("unit")

            build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces)

            for build_item in build_items:
                id = build_item.get("objectid")
                object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
                if "type" in object.attrib:
                    if object.attrib["type"] == "support" or object.attrib["type"] == "other":
                        # Ignore support objects, as cura does not support these.
                        # We can't guarantee that they wont be made solid.
                        # We also ignore "other", as I have no idea what to do with them.
                        Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"])
                        continue
                    elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model":
                        pass  # Load these as normal
                    else:
                        # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway.
                        Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec",
                                   object.attrib["type"])
                        continue

                build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id))

                # compensate for original center position, if object(s) is/are not around its zero position
                transform_matrix = Matrix()
                mesh_data = build_item_node.getMeshData()
                if mesh_data is not None:
                    extents = mesh_data.getExtents()
                    center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
                    transform_matrix.setByTranslation(center_vector)

                # offset with transform from 3mf
                transform = build_item.get("transform")
                if transform is not None:
                    transform_matrix.multiply(self._createMatrixFromTransformationString(transform))

                build_item_node.setTransformation(transform_matrix)

                global_container_stack = UM.Application.getInstance().getGlobalContainerStack()

                # Create a transformation Matrix to convert from 3mf worldspace into ours.
                # First step: flip the y and z axis.
                transformation_matrix = Matrix()
                transformation_matrix._data[1, 1] = 0
                transformation_matrix._data[1, 2] = 1
                transformation_matrix._data[2, 1] = -1
                transformation_matrix._data[2, 2] = 0

                # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
                # build volume.
                if global_container_stack:
                    translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
                                                y = -global_container_stack.getProperty("machine_depth", "value") / 2,
                                                z = 0)
                    translation_matrix = Matrix()
                    translation_matrix.setByTranslation(translation_vector)
                    transformation_matrix.multiply(translation_matrix)

                # Third step: 3MF also defines a unit, wheras Cura always assumes mm.
                scale_matrix = Matrix()
                scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
                transformation_matrix.multiply(scale_matrix)

                # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
                build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix))

                result.append(build_item_node)

        except Exception as e:
            Logger.log("e", "An exception occurred in 3mf reader: %s", e)

        return result
예제 #10
0
파일: Camera.py 프로젝트: Ultimaker/Uranium
class Camera(SceneNode.SceneNode):
    def __init__(self, name: str = "", parent: SceneNode.SceneNode = None) -> None:
        super().__init__(parent)
        self._name = name  # type: str
        self._projection_matrix = Matrix()  # type: Matrix
        self._projection_matrix.setOrtho(-5, 5, 5, -5, -100, 100)
        self._perspective = True  # type: bool
        self._viewport_width = 0  # type: int
        self._viewport_height = 0  # type: int
        self._window_width = 0  # type: int
        self._window_height = 0  # type: int
        self._auto_adjust_view_port_size = True  # type: bool
        self.setCalculateBoundingBox(False)
        self._cached_view_projection_matrix = None # type: Optional[Matrix]

    def __deepcopy__(self, memo: Dict[int, object]) -> "Camera":
        copy = cast(Camera, super().__deepcopy__(memo))
        copy._projection_matrix = self._projection_matrix
        copy._window_height = self._window_height
        copy._window_width = self._window_width
        copy._viewport_height = self._viewport_height
        copy._viewport_width = self._viewport_width
        return copy

    def setMeshData(self, mesh_data: Optional["MeshData"]) -> None:
        assert mesh_data is None, "Camera's can't have mesh data"

    def getAutoAdjustViewPort(self) -> bool:
        return self._auto_adjust_view_port_size

    def setAutoAdjustViewPort(self, auto_adjust: bool) -> None:
        self._auto_adjust_view_port_size = auto_adjust

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self) -> Matrix:
        return self._projection_matrix
    
    def getViewportWidth(self) -> int:
        return self._viewport_width
    
    def setViewportWidth(self, width: int) -> None:
        self._viewport_width = width
    
    def setViewportHeight(self, height: int) -> None:
        self._viewport_height = height
        
    def setViewportSize(self, width: int, height: int) -> None:
        self._viewport_width = width
        self._viewport_height = height

    def getViewProjectionMatrix(self):
        if self._cached_view_projection_matrix is None:
            inverted_transformation = self.getWorldTransformation()
            inverted_transformation.invert()
            self._cached_view_projection_matrix = self._projection_matrix.multiply(inverted_transformation, copy = True)
        return self._cached_view_projection_matrix

    def _updateWorldTransformation(self):
        self._cached_view_projection_matrix = None
        super()._updateWorldTransformation()
    
    def getViewportHeight(self) -> int:
        return self._viewport_height

    def setWindowSize(self, width: int, height: int) -> None:
        self._window_width = width
        self._window_height = height

    def getWindowSize(self) -> Tuple[int, int]:
        return self._window_width, self._window_height

    def render(self, renderer) -> bool:
        # It's a camera. It doesn't need rendering.
        return True
    
    ##  Set the projection matrix of this camera.
    #   \param matrix The projection matrix to use for this camera.
    def setProjectionMatrix(self, matrix: Matrix) -> None:
        self._projection_matrix = matrix
        self._cached_view_projection_matrix = None

    def isPerspective(self) -> bool:
        return self._perspective

    def setPerspective(self, perspective: bool) -> None:
        self._perspective = perspective

    ##  Get a ray from the camera into the world.
    #
    #   This will create a ray from the camera's origin, passing through (x, y)
    #   on the near plane and continuing based on the projection matrix.
    #
    #   \param x The X coordinate on the near plane this ray should pass through.
    #   \param y The Y coordinate on the near plane this ray should pass through.
    #
    #   \return A Ray object representing a ray from the camera origin through X, Y.
    #
    #   \note The near-plane coordinates should be in normalized form, that is within (-1, 1).
    def getRay(self, x: float, y: float) -> Ray:
        window_x = ((x + 1) / 2) * self._window_width
        window_y = ((y + 1) / 2) * self._window_height
        view_x = (window_x / self._viewport_width) * 2 - 1
        view_y = (window_y / self._viewport_height) * 2 - 1

        inverted_projection = numpy.linalg.inv(self._projection_matrix.getData().copy())
        transformation = self.getWorldTransformation().getData()

        near = numpy.array([view_x, -view_y, -1.0, 1.0], dtype = numpy.float32)
        near = numpy.dot(inverted_projection, near)
        near = numpy.dot(transformation, near)
        near = near[0:3] / near[3]

        far = numpy.array([view_x, -view_y, 1.0, 1.0], dtype = numpy.float32)
        far = numpy.dot(inverted_projection, far)
        far = numpy.dot(transformation, far)
        far = far[0:3] / far[3]

        direction = far - near
        direction /= numpy.linalg.norm(direction)

        return Ray(self.getWorldPosition(), Vector(-direction[0], -direction[1], -direction[2]))

    ##  Project a 3D position onto the 2D view plane.
    def project(self, position: Vector) -> Tuple[float, float]:
        projection = self._projection_matrix
        view = self.getWorldTransformation()
        view.invert()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)
        return position.x / position.z / 2.0, position.y / position.z / 2.0
예제 #11
0
class SceneNode():
    class TransformSpace:
        Local = 1
        Parent = 2
        World = 3

    ##  Construct a scene node.
    #   \param parent The parent of this node (if any). Only a root node should have None as a parent.
    #   \param kwargs Keyword arguments.
    #                 Possible keywords:
    #                 - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True
    #                 - name \type{string} Name of the SceneNode. Defaults to empty string.
    def __init__(self, parent = None, **kwargs):
        super().__init__()  # Call super to make multiple inheritance work.

        self._children = []     # type: List[SceneNode]
        self._mesh_data = None  # type: MeshData

        # Local transformation (from parent to local)
        self._transformation = Matrix()  # type: Matrix

        # Convenience "components" of the transformation
        self._position = Vector()  # type: Vector
        self._scale = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._shear = Vector(0.0, 0.0, 0.0)  # type: Vector
        self._mirror = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._orientation = Quaternion()  # type: Quaternion

        # World transformation (from root to local)
        self._world_transformation = Matrix()  # type: Matrix

        # Convenience "components" of the world_transformation
        self._derived_position = Vector()  # type: Vector
        self._derived_orientation = Quaternion()  # type: Quaternion
        self._derived_scale = Vector()  # type: Vector

        self._parent = parent  # type: Optional[SceneNode]

        # Can this SceneNode be modified in any way?
        self._enabled = True  # type: bool
        # Can this SceneNode be selected in any way?
        self._selectable = False  # type: bool

        # Should the AxisAlignedBounxingBox be re-calculated?
        self._calculate_aabb = True  # type: bool

        # The AxisAligned bounding box.
        self._aabb = None  # type: Optional[AxisAlignedBox]
        self._bounding_box_mesh = None  # type: Optional[MeshData]

        self._visible = kwargs.get("visible", True)  # type: bool
        self._name = kwargs.get("name", "")  # type: str
        self._decorators = []  # type: List[SceneNodeDecorator]

        ## Signals
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo):
        copy = SceneNode()
        copy.setTransformation(self.getLocalTransformation())
        copy.setMeshData(self._mesh_data)
        copy.setVisible(deepcopy(self._visible, memo))
        copy._selectable = deepcopy(self._selectable, memo)
        copy._name = deepcopy(self._name, memo)
        for decorator in self._decorators:
            copy.addDecorator(deepcopy(decorator, memo))

        for child in self._children:
            copy.addChild(deepcopy(child, memo))
        self.calculateBoundingBoxMesh()
        return copy

    ##  Set the center position of this node.
    #   This is used to modify it's mesh data (and it's children) in such a way that they are centered.
    #   In most cases this means that we use the center of mass as center (which most objects don't use)
    def setCenterPosition(self, center: Vector):
        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m).set(center_position=center)
        for child in self._children:
            child.setCenterPosition(center)

    ##  \brief Get the parent of this node. If the node has no parent, it is the root node.
    #   \returns SceneNode if it has a parent and None if it's the root node.
    def getParent(self) -> Optional["SceneNode"]:
        return self._parent

    def getMirror(self) -> Vector:
        return self._mirror

    ##  Get the MeshData of the bounding box
    #   \returns \type{MeshData} Bounding box mesh.
    def getBoundingBoxMesh(self) -> Optional[MeshData]:
        return self._bounding_box_mesh

    ##  (re)Calculate the bounding box mesh.
    def calculateBoundingBoxMesh(self):
        aabb = self.getBoundingBox()
        if aabb:
            bounding_box_mesh = MeshBuilder()
            rtf = aabb.maximum
            lbb = aabb.minimum

            bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z)  # Left - Top - Front

            bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z)  # Left - Bottom - Front

            bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z)  # Right - Bottom - Front

            bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z)  # Right - Top - Front

            bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z)  # Right - Top - Back
            bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z)  # Left - Top - Back
            bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z)  # Left - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z)  # Right - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z)  # Right - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z)  # Right - Bottom - Back

            self._bounding_box_mesh = bounding_box_mesh.build()

    ##  Handler for the ParentChanged signal
    #   \param node Node from which this event was triggered.
    def _onParentChanged(self, node: Optional["SceneNode"]):
        for child in self.getChildren():
            child.parentChanged.emit(self)

    ##  Signal for when a \type{SceneNodeDecorator} is added / removed.
    decoratorsChanged = Signal()

    ##  Add a SceneNodeDecorator to this SceneNode.
    #   \param \type{SceneNodeDecorator} decorator The decorator to add.
    def addDecorator(self, decorator: SceneNodeDecorator):
        if type(decorator) in [type(dec) for dec in self._decorators]:
            Logger.log("w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator))
            return
        try:
            decorator.setNode(self)
        except AttributeError:
            Logger.logException("e", "Unable to add decorator.")
            return
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    ##  Get all SceneNodeDecorators that decorate this SceneNode.
    #   \return list of all SceneNodeDecorators.
    def getDecorators(self) -> List[SceneNodeDecorator]:
        return self._decorators

    ##  Get SceneNodeDecorators by type.
    #   \param dec_type type of decorator to return.
    def getDecorator(self, dec_type) -> Optional[SceneNodeDecorator]:
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator

    ##  Remove all decorators
    def removeDecorators(self):
        for decorator in self._decorators:
            decorator.clear()
        self._decorators = []
        self.decoratorsChanged.emit(self)

    ##  Remove decorator by type.
    #   \param dec_type type of the decorator to remove.
    def removeDecorator(self, dec_type: SceneNodeDecorator):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                decorator.clear()
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    ##  Call a decoration of this SceneNode.
    #   SceneNodeDecorators add Decorations, which are callable functions.
    #   \param \type{string} function The function to be called.
    #   \param *args
    #   \param **kwargs
    def callDecoration(self, function: str, *args, **kwargs):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e))
                    return None

    ##  Does this SceneNode have a certain Decoration (as defined by a Decorator)
    #   \param \type{string} function the function to check for.
    def hasDecoration(self, function: str) -> bool:
        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self) -> str:
        return self._name

    def setName(self, name: str):
        self._name = name

    ##  How many nodes is this node removed from the root?
    #   \return |tupe{int} Steps from root (0 means it -is- the root).
    def getDepth(self) -> int:
        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    ##  \brief Set the parent of this object
    #   \param scene_node SceneNode that is the parent of this object.
    def setParent(self, scene_node: Optional["SceneNode"]):
        if self._parent:
            self._parent.removeChild(self)

        if scene_node:
            scene_node.addChild(self)

    ##  Emitted whenever the parent changes.
    parentChanged = Signal()

    ##  \brief Get the visibility of this node. The parents visibility overrides the visibility.
    #   TODO: Let renderer actually use the visibility to decide whether to render or not.
    def isVisible(self) -> bool:
        if self._parent is not None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    ##  Set the visibility of this SceneNode.
    def setVisible(self, visible: bool):
        self._visible = visible

    ##  \brief Get the (original) mesh data from the scene node/object.
    #   \returns MeshData
    def getMeshData(self) -> Optional[MeshData]:
        return self._mesh_data

    ##  \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.
    #   \returns MeshData
    def getMeshDataTransformed(self) -> Optional[MeshData]:
        if self._mesh_data:
            return self._mesh_data.getTransformed(self.getWorldTransformation())
        return self._mesh_data

    ##  \brief Set the mesh of this node/object
    #   \param mesh_data MeshData object
    def setMeshData(self, mesh_data: Optional[MeshData]):
        self._mesh_data = mesh_data
        self._resetAABB()
        self.meshDataChanged.emit(self)

    ##  Emitted whenever the attached mesh data object changes.
    meshDataChanged = Signal()

    def _onMeshDataChanged(self):
        self.meshDataChanged.emit(self)

    ##  \brief Add a child to this node and set it's parent as this node.
    #   \params scene_node SceneNode to add.
    def addChild(self, scene_node: "SceneNode"):
        if scene_node not in self._children:
            scene_node.transformationChanged.connect(self.transformationChanged)
            scene_node.childrenChanged.connect(self.childrenChanged)
            scene_node.meshDataChanged.connect(self.meshDataChanged)

            self._children.append(scene_node)
            self._resetAABB()
            self.childrenChanged.emit(self)

            if not scene_node._parent is self:
                scene_node._parent = self
                scene_node._transformChanged()
                scene_node.parentChanged.emit(self)

    ##  \brief remove a single child
    #   \param child Scene node that needs to be removed.
    def removeChild(self, child: "SceneNode"):
        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self._resetAABB()
        self.childrenChanged.emit(self)

    ##  \brief Removes all children and its children's children.
    def removeAllChildren(self):
        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    ##  \brief Get the list of direct children
    #   \returns List of children
    def getChildren(self) -> List["SceneNode"]:
        return self._children

    def hasChildren(self) -> bool:
        return True if self._children else False

    ##  \brief Get list of all children (including it's children children children etc.)
    #   \returns list ALl children in this 'tree'
    def getAllChildren(self) -> List["SceneNode"]:
        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    ##  \brief Emitted whenever the list of children of this object or any child object changes.
    #   \param object The object that triggered the change.
    childrenChanged = Signal()

    ##  \brief Computes and returns the transformation from world to local space.
    #   \returns 4x4 transformation matrix
    def getWorldTransformation(self) -> Matrix:
        if self._world_transformation is None:
            self._updateTransformation()

        return deepcopy(self._world_transformation)

    ##  \brief Returns the local transformation with respect to its parent. (from parent to local)
    #   \retuns transformation 4x4 (homogenous) matrix
    def getLocalTransformation(self) -> Matrix:
        if self._transformation is None:
            self._updateTransformation()

        return deepcopy(self._transformation)

    def setTransformation(self, transformation: Matrix):
        self._transformation = deepcopy(transformation) # Make a copy to ensure we never change the given transformation
        self._transformChanged()

    ##  Get the local orientation value.
    def getOrientation(self) -> Quaternion:
        return deepcopy(self._orientation)

    def getWorldOrientation(self) -> Quaternion:
        return deepcopy(self._derived_orientation)

    ##  \brief Rotate the scene object (and thus its children) by given amount
    #
    #   \param rotation \type{Quaternion} A quaternion indicating the amount of rotation.
    #   \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
    def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return

        orientation_matrix = rotation.toMatrix()
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(orientation_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local orientation of this scene node.
    #
    #   \param orientation \type{Quaternion} The new orientation of this scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local):
        if not self._enabled or orientation == self._orientation:
            return

        new_transform_matrix = Matrix()
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldOrientation() == orientation:
                return
            new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse()
            orientation_matrix = new_orientation.toMatrix()
        else:  # Local
            orientation_matrix = orientation.toMatrix()

        euler_angles = orientation_matrix.getEuler()

        new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear)
        self._transformation = new_transform_matrix
        self._transformChanged()

    ##  Get the local scaling value.
    def getScale(self) -> Vector:
        return self._scale

    def getWorldScale(self) -> Vector:
        return self._derived_scale

    ##  Scale the scene object (and thus its children) by given amount
    #
    #   \param scale \type{Vector} A Vector with three scale values
    #   \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
    def scale(self, scale: Vector, transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return

        scale_matrix = Matrix()
        scale_matrix.setByScaleVector(scale)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(scale_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local scale value.
    #
    #   \param scale \type{Vector} The new scale value of the scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local):
        if not self._enabled or scale == self._scale:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.scale(scale / self._scale, SceneNode.TransformSpace.Local)
            return
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldScale() == scale:
                return

            self.scale(scale / self._scale, SceneNode.TransformSpace.World)

    ##  Get the local position.
    def getPosition(self) -> Vector:
        return self._position

    ##  Get the position of this scene node relative to the world.
    def getWorldPosition(self) -> Vector:
        return self._derived_position

    ##  Translate the scene object (and thus its children) by given amount.
    #
    #   \param translation \type{Vector} The amount to translate by.
    #   \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
    def translate(self, translation: Vector, transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return
        translation_matrix = Matrix()
        translation_matrix.setByTranslation(translation)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            world_transformation = deepcopy(self._world_transformation)
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(translation_matrix)
            self._transformation.multiply(world_transformation)
        self._transformChanged()

    ##  Set the local position value.
    #
    #   \param position The new position value of the SceneNode.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setPosition(self, position: Vector, transform_space: int = TransformSpace.Local):
        if not self._enabled or position == self._position:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.translate(position - self._position, SceneNode.TransformSpace.Parent)
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldPosition() == position:
                return
            self.translate(position - self._derived_position, SceneNode.TransformSpace.World)

    ##  Signal. Emitted whenever the transformation of this object or any child object changes.
    #   \param object The object that caused the change.
    transformationChanged = Signal()

    ##  Rotate this scene node in such a way that it is looking at target.
    #
    #   \param target \type{Vector} The target to look at.
    #   \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
    def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalized()
        up = up.normalized()
        s = f.cross(up).normalized()
        u = s.cross(f).normalized()

        m = Matrix([
            [ s.x,  u.x,  -f.x, 0.0],
            [ s.y,  u.y,  -f.y, 0.0],
            [ s.z,  u.z,  -f.z, 0.0],
            [ 0.0,  0.0,  0.0,  1.0]
        ])

        self.setOrientation(Quaternion.fromMatrix(m))

    ##  Can be overridden by child nodes if they need to perform special rendering.
    #   If you need to handle rendering in a special way, for example for tool handles,
    #   you can override this method and render the node. Return True to prevent the
    #   view from rendering any attached mesh data.
    #
    #   \param renderer The renderer object to use for rendering.
    #
    #   \return False if the view should render this node, True if we handle our own rendering.
    def render(self, renderer) -> bool:
        return False

    ##  Get whether this SceneNode is enabled, that is, it can be modified in any way.
    def isEnabled(self) -> bool:
        return self._enabled

    ##  Set whether this SceneNode is enabled.
    #   \param enable True if this object should be enabled, False if not.
    #   \sa isEnabled
    def setEnabled(self, enable: bool):
        self._enabled = enable

    ##  Get whether this SceneNode can be selected.
    #
    #   \note This will return false if isEnabled() returns false.
    def isSelectable(self) -> bool:
        return self._enabled and self._selectable

    ##  Set whether this SceneNode can be selected.
    #
    #   \param select True if this SceneNode should be selectable, False if not.
    def setSelectable(self, select: bool):
        self._selectable = select

    ##  Get the bounding box of this node and its children.
    def getBoundingBox(self) -> Optional[AxisAlignedBox]:
        if not self._calculate_aabb:
            return None
        if self._aabb is None:
            self._calculateAABB()
        return self._aabb

    ##  Set whether or not to calculate the bounding box for this node.
    #
    #   \param calculate True if the bounding box should be calculated, False if not.
    def setCalculateBoundingBox(self, calculate: bool):
        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    def getShear(self) -> Vector:
        return self._shear

    ##  private:
    def _transformChanged(self):
        self._updateTransformation()
        self._resetAABB()
        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateTransformation(self):
        scale, shear, euler_angles, translation, mirror = self._transformation.decompose()
        self._position = translation
        self._scale = scale
        self._shear = shear
        self._mirror = mirror
        orientation = Quaternion()
        euler_angle_matrix = Matrix()
        euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z)
        orientation.setByMatrix(euler_angle_matrix)
        self._orientation = orientation
        if self._parent:
            self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True)
        else:
            self._world_transformation = self._transformation

        world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose()
        self._derived_position = world_translation
        self._derived_scale = world_scale

        world_euler_angle_matrix = Matrix()
        world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z)
        self._derived_orientation.setByMatrix(world_euler_angle_matrix)

    def _resetAABB(self):
        if not self._calculate_aabb:
            return
        self._aabb = None
        if self.getParent():
            self.getParent()._resetAABB()
        self.boundingBoxChanged.emit()

    def _calculateAABB(self):
        aabb = None
        if self._mesh_data:
            aabb = self._mesh_data.getExtents(self.getWorldTransformation())
        else:  # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
            position = self.getWorldPosition()
            aabb = AxisAlignedBox(minimum = position, maximum = position)

        for child in self._children:
            if aabb is None:
                aabb = child.getBoundingBox()
            else:
                aabb = aabb + child.getBoundingBox()
        self._aabb = aabb
예제 #12
0
class CliParser:

    def __init__(self) -> None:
        SteSlicerApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
        self._is_layers_in_file = False
        self._cancelled = False
        self._message = None
        self._layer_number = -1
        self._extruder_number = 0
        self._pi_faction = 0
        self._position = Position
        self._gcode_position = Position
        # stack to get print settingd via getProperty method
        self._application = SteSlicerApplication.getInstance()
        self._global_stack = self._application.getGlobalContainerStack() #type: GlobalStack
        self._licensed = self._application.getLicenseManager().licenseValid

        self._rot_nwp = Matrix()
        self._rot_nws = Matrix()

        self._scene_node = None
        
        self._extruder_number = 0
        # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._extruder_offsets = {}
        self._gcode_list = []
        self._current_layer_thickness = 0
        self._current_layer_height = 0

        #speeds
        self._travel_speed = 0
        self._wall_0_speed = 0
        self._skin_speed = 0
        self._infill_speed = 0
        self._support_speed = 0
        self._retraction_speed = 0
        self._prime_speed = 0

        #retraction
        self._enable_retraction = False
        self._retraction_amount = 0
        self._retraction_min_travel = 1.5
        self._retraction_hop_enabled = False
        self._retraction_hop = 1

        self._filament_diameter = 1.75
        self._line_width = 0.4
        self._layer_thickness = 0.2
        self._clearValues()

    _layer_keyword = "$$LAYER/"
    _geometry_end_keyword = "$$GEOMETRYEND"
    _body_type_keyword = "//body//"
    _support_type_keyword = "//support//"
    _skin_type_keyword = "//skin//"
    _infill_type_keyword = "//infill//"
    _perimeter_type_keyword = "//perimeter//"

    _type_keyword = ";TYPE:"

    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 _setPrintSettings(self):
        pass

    def _onHideMessage(self, message: Optional[Union[str, Message]]) -> None:
        if message == self._message:
            self._cancelled = True

    def _clearValues(self):
        self._extruder_number = 0
        self._layer_number = -1
        self._layer_data_builder = LayerDataBuilder()
        self._pi_faction = 0
        self._position = Position(0,0,0,0,0,1,0,[0])
        self._gcode_position = Position(0, 0, 0, 0, 0, 0, 0, [0])
        self._rot_nwp = Matrix()
        self._rot_nws = Matrix()
        self._layer_type = LayerPolygon.Inset0Type

        self._parsing_type = self._global_stack.getProperty(
            "printing_mode", "value")
        self._line_width = self._global_stack.getProperty("wall_line_width_0", "value")
        self._layer_thickness = self._global_stack.getProperty("layer_height", "value")

        self._travel_speed = self._global_stack.getProperty(
            "speed_travel", "value")
        self._wall_0_speed = self._global_stack.getProperty(
            "speed_wall_0", "value")
        self._skin_speed = self._global_stack.getProperty(
            "speed_topbottom", "value")
        self._infill_speed = self._global_stack.getProperty("speed_infill", "value")
        self._support_speed = self._global_stack.getProperty(
            "speed_support", "value")
        self._retraction_speed = self._global_stack.getProperty(
            "retraction_retract_speed", "value")
        self._prime_speed = self._global_stack.getProperty(
            "retraction_prime_speed", "value")

        extruder = self._global_stack.extruders.get("%s" % self._extruder_number, None) #type: Optional[ExtruderStack]
        
        self._filament_diameter = extruder.getProperty(
            "material_diameter", "value")
        self._enable_retraction = extruder.getProperty(
            "retraction_enable", "value")
        self._retraction_amount = extruder.getProperty(
            "retraction_amount", "value")
        self._retraction_min_travel = extruder.getProperty(
            "retraction_min_travel", "value")
        self._retraction_hop_enabled = extruder.getProperty(
            "retraction_hop_enabled", "value")
        self._retraction_hop = extruder.getProperty(
            "retraction_hop", "value")

    def _transformCoordinates(self, x: float, y: float, z: float, i: float, j: float, k: float, position: Position) -> (float, float, float, float, float, float):
        a = position.a
        c = position.c
        # Get coordinate angles
        if abs(self._position.c - k) > 0.00001:
            a = math.acos(k)
            self._rot_nwp = Matrix()
            self._rot_nwp.setByRotationAxis(-a, Vector.Unit_X)
            a = degrees(a)
        if abs(self._position.a - i) > 0.00001 or abs(self._position.b - j) > 0.00001:
            c = numpy.arctan2(j, i) if x != 0 and y != 0 else 0
            angle = degrees(c + self._pi_faction * 2 * math.pi)
            if abs(angle - position.c) > 180:
                self._pi_faction += 1 if (angle - position.c) < 0 else -1
            c += self._pi_faction * 2 * math.pi
            c -= math.pi / 2
            self._rot_nws = Matrix()
            self._rot_nws.setByRotationAxis(c, Vector.Unit_Z)
            c = degrees(c)
        
        #tr = self._rot_nws.multiply(self._rot_nwp, True)
        tr = self._rot_nws.multiply(self._rot_nwp, True)
        #tr = tr.multiply(self._rot_nwp)
        tr.invert()
        pt = Vector(data=numpy.array([x, y, z, 1]))
        ret = tr.multiply(pt, True).getData()
        return Position(ret[0], ret[1], ret[2], a, 0, c, 0, [0])

    @staticmethod
    def _getValue(line: str, key: str) -> Optional[str]:
        n = line.find(key)
        if n < 0:
            return None
        n += len(key)
        splitted = line[n:].split("/")
        if len(splitted) > 1:
            return splitted[1]
        else:
            return None

    def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[8] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(
                self._layer_number, self._current_layer_height)
            self._layer_data_builder.setLayerThickness(
                self._layer_number, layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 6), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:

            points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1],
                            -point[4], point[5], -point[3]]
            extrusion_values[i] = point[7]
            if i > 0:
                line_feedrates[i - 1] = point[6]
                line_types[i - 1] = point[8]
                if point[8] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
                    line_widths[i - 1] = 0.1
                    # Travels are set as zero thickness lines
                    line_thicknesses[i - 1] = 0.0
                else:
                    line_widths[i - 1] = self._line_width
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types,
                                 points, line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def processPolyline(self, line: str, path: List[List[Union[float, int]]], gcode_list: List[str]) -> bool:
        # Convering line to point array
        values_line = self._getValue(line, "$$POLYLINE")
        if not values_line:
            return (self._position, None)
        values = values_line.split(",")
        if len(values[3:]) % 2 != 0:
            return (self._position, None)
        idx = 2
        points = values[3:]
        if len(points) < 2:
            return (self._position, None)
        # TODO: add combing to this polyline
        new_position, new_gcode_position = self._cliPointToPosition(
            CliPoint(float(points[0]), float(points[1])), self._position, False)
        
        is_retraction = self._enable_retraction and self._positionLength(
            self._position, new_position) > self._retraction_min_travel
        if is_retraction:
            #we have retraction move
            new_extruder_position = self._position.e[self._extruder_number] - self._retraction_amount
            gcode_list.append("G1 E%.5f F%.0f\n" % (new_extruder_position, (self._retraction_speed * 60)))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[self._extruder_number] = new_extruder_position
            path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                         self._position.c, self._retraction_speed, self._position.e, LayerPolygon.MoveRetractionType])
        
            if self._retraction_hop_enabled:
                    #add hop movement
                    gx, gy, gz, ga, gb, gc, gf, ge = self._gcode_position
                    x, y, z, a, b, c, f, e = self._position
                    gcode_position = Position(
                        gx, gy, gz + self._retraction_hop, ga, gb, gc, self._travel_speed, ge)
                    self._position = Position(
                        x + a * self._retraction_hop, y + b * self._retraction_hop, z + c * self._retraction_hop, a, b, c, self._travel_speed, e)
                    gcode_command = self._generateGCodeCommand(
                        0, gcode_position, self._travel_speed)
                    if gcode_command is not None:
                        gcode_list.append(gcode_command)
                    self._gcode_position = gcode_position
                    path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                                 self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveCombingType])
                    gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
                    x, y, z, a, b, c, f, e = new_position
                    gcode_position = Position(
                        gx, gy, gz + self._retraction_hop, ga, gb, gc, self._travel_speed, ge)
                    position = Position(
                        x + a * self._retraction_hop, y + b * self._retraction_hop, z + c * self._retraction_hop, a, b, c, self._travel_speed, e)
                    gcode_command = self._generateGCodeCommand(
                        0, gcode_position, self._travel_speed)
                    if gcode_command is not None:
                        gcode_list.append(gcode_command)
                    path.append([position.x, position.y, position.z, position.a, position.b,
                                 position.c, position.f, position.e, LayerPolygon.MoveCombingType])

        feedrate = self._travel_speed
        x, y, z, a, b, c, f, e = new_position
        self._position = Position(x, y, z, a, b, c, feedrate, self._position.e)
        gcode_command = self._generateGCodeCommand(0, new_gcode_position, feedrate)
        if gcode_command is not None:
            gcode_list.append(gcode_command)
        gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
        self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate, ge)
        path.append([x, y, z, a, b, c, feedrate, e,
                     LayerPolygon.MoveCombingType])
        
        if is_retraction:
            #we have retraction move
            new_extruder_position = self._position.e[self._extruder_number] + self._retraction_amount
            gcode_list.append("G1 E%.5f F%.0f\n" % (new_extruder_position, (self._prime_speed * 60)))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[self._extruder_number] = new_extruder_position
            path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                         self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveRetractionType])
            
        if self._layer_type == LayerPolygon.SupportType:
            gcode_list.append(self._type_keyword + "SUPPORT\n")
        elif self._layer_type == LayerPolygon.SkinType:
            gcode_list.append(self._type_keyword + "SKIN\n")
        elif self._layer_type == LayerPolygon.InfillType:
            gcode_list.append(self._type_keyword + "FILL\n")
        else:
            gcode_list.append(self._type_keyword + "WALL-OUTER\n")

        while idx < len(points):
            point = CliPoint(float(points[idx]), float(points[idx + 1]))
            idx += 2
            new_position, new_gcode_position = self._cliPointToPosition(point, self._position)
            feedrate = self._wall_0_speed
            if self._layer_type == LayerPolygon.SupportType:
                feedrate = self._support_speed
            elif self._layer_type == LayerPolygon.SkinType:
                feedrate = self._skin_speed
            elif self._layer_type == LayerPolygon.InfillType:
                feedrate = self._infill_speed
            x, y, z, a, b, c, f, e = new_position
            self._position = Position(x, y, z, a, b, c, feedrate, e)
            gcode_command = self._generateGCodeCommand(1, new_gcode_position, feedrate)
            if gcode_command is not None:
                gcode_list.append(gcode_command)
            gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
            self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate, ge)
            path.append([x,y,z,a,b,c, feedrate, e, self._layer_type])

    def _generateGCodeCommand(self, g: int, gcode_position: Position, feedrate: float) -> Optional[str]:
            gcode_command = "G%s" % g
            if abs(gcode_position.x - self._gcode_position.x) > 0.0001:
                gcode_command += " X%.2f" % gcode_position.x
            if abs(gcode_position.y - self._gcode_position.y) > 0.0001:
                gcode_command += " Y%.2f" % gcode_position.y
            if abs(gcode_position.z - self._gcode_position.z) > 0.0001:
                gcode_command += " Z%.2f" % gcode_position.z
            if abs(gcode_position.a - self._gcode_position.a) > 0.0001:
                gcode_command += " A%.2f" % gcode_position.a
            if abs(gcode_position.b - self._gcode_position.b) > 0.0001:
                gcode_command += " B%.2f" % gcode_position.b
            if abs(gcode_position.c - self._gcode_position.c) > 0.0001:
                gcode_command += " C%.2f" % gcode_position.c
            if abs(feedrate - self._gcode_position.f) > 0.0001:
                gcode_command += " F%.0f" % (feedrate * 60)
            if abs(gcode_position.e[self._extruder_number] - self._gcode_position.e[self._extruder_number]) > 0.0001 and g > 0:
                gcode_command += " E%.5f" % gcode_position.e[self._extruder_number]
            gcode_command += "\n"
            if gcode_command != "G%s\n" % g:
                return gcode_command
            else:
                return None
        
    def _calculateExtrusion(self, current_point: List[float], previous_point: Position) -> float:
        
        Af = (self._filament_diameter / 2) ** 2 * numpy.pi
        Al = self._line_width * self._layer_thickness
        de = numpy.sqrt((current_point[0] - previous_point[0])
                        ** 2 + (current_point[1] - previous_point[1])**2 +
                         (current_point[2] - previous_point[2])**2)
        dVe = Al * de
        return dVe / Af

    def _writeStartCode(self, gcode_list: List[str]):
        gcode_list.append("T0\n")
        init_temperature = self._global_stack.getProperty(
            "material_initial_print_temperature", "value")
        init_bed_temperature = self._global_stack.getProperty(
            "material_bed_temperature_layer_0", "value")
        gcode_list.extend(["M140 S%s\n" % init_bed_temperature,
                           "M105\n",
                           "M190 S%s\n" % init_bed_temperature,
                           "M104 S%s\n" % init_temperature,
                           "M105\n",
                           "M109 S%s\n" % init_temperature,
                           "M82 ;absolute extrusion mode\n"])
        start_gcode = self._global_stack.getProperty(
            "machine_start_gcode", "value")
        gcode_list.append(start_gcode + "\n")

    def _cliPointToPosition(self, point: CliPoint, position: Position, extrusion_move: bool = True) -> (Position, Position):
        x, y, z, i, j, k = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
        if self._parsing_type == "classic":
            x = point.x
            y = point.y
            z = self._current_layer_height
            i = 0
            j = 0
            k = 1
        elif self._parsing_type == "cylindrical":
            x = self._current_layer_height * math.cos(point.y)
            y = self._current_layer_height * math.sin(point.y)
            z = point.x
            length = numpy.sqrt(x**2 + y**2)
            i = x / length if length != 0 else 0
            j = y / length if length != 0 else 0
            k = 0
        new_position = Position(x,y,z,i,j,k,0, [0])
        new_gcode_position = self._transformCoordinates(x,y,z,i,j,k, self._gcode_position)
        new_position.e[self._extruder_number] = position.e[self._extruder_number] + self._calculateExtrusion([x,y,z], position) if extrusion_move else position.e[self._extruder_number]
        new_gcode_position.e[self._extruder_number] = new_position.e[self._extruder_number]
        return new_position, new_gcode_position

    @staticmethod
    def _positionLength(start: Position, end: Position) -> float:
        return numpy.sqrt((start.x - end.x)**2 + (start.y - end.y)**2 + (start.z - end.z)**2)
예제 #13
0
class SceneNode:
    """A scene node object.

    These objects can hold a mesh and multiple children. Each node has a transformation matrix
    that maps it it's parents space to the local space (it's inverse maps local space to parent).

    SceneNodes can be "Decorated" by adding SceneNodeDecorator objects.
    These decorators can add functionality to scene nodes.
    :sa SceneNodeDecorator
    :todo Add unit testing
    """
    class TransformSpace:
        Local = 1  #type: int
        Parent = 2  #type: int
        World = 3  #type: int

    def __init__(self,
                 parent: Optional["SceneNode"] = None,
                 visible: bool = True,
                 name: str = "",
                 node_id: str = "") -> None:
        """Construct a scene node.

        :param parent: The parent of this node (if any). Only a root node should have None as a parent.
        :param visible: Is the SceneNode (and thus, all its children) visible?
        :param name: Name of the SceneNode.
        """

        super().__init__()  # Call super to make multiple inheritance work.

        self._children = []  # type: List[SceneNode]
        self._mesh_data = None  # type: Optional[MeshData]

        # Local transformation (from parent to local)
        self._transformation = Matrix()  # type: Matrix

        # Convenience "components" of the transformation
        self._position = Vector()  # type: Vector
        self._scale = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._shear = Vector(0.0, 0.0, 0.0)  # type: Vector
        self._mirror = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._orientation = Quaternion()  # type: Quaternion

        # World transformation (from root to local)
        self._world_transformation = Matrix()  # type: Matrix

        # This is used for rendering. Since we don't want to recompute it every time, we cache it in the node
        self._cached_normal_matrix = Matrix()

        # Convenience "components" of the world_transformation
        self._derived_position = Vector()  # type: Vector
        self._derived_orientation = Quaternion()  # type: Quaternion
        self._derived_scale = Vector()  # type: Vector

        self._parent = parent  # type: Optional[SceneNode]

        # Can this SceneNode be modified in any way?
        self._enabled = True  # type: bool
        # Can this SceneNode be selected in any way?
        self._selectable = False  # type: bool

        # Should the AxisAlignedBoundingBox be re-calculated?
        self._calculate_aabb = True  # type: bool

        # The AxisAligned bounding box.
        self._aabb = None  # type: Optional[AxisAlignedBox]
        self._bounding_box_mesh = None  # type: Optional[MeshData]

        self._visible = visible  # type: bool
        self._name = name  # type: str
        self._id = node_id  # type: str
        self._decorators = []  # type: List[SceneNodeDecorator]

        # Store custom settings to be compatible with Savitar SceneNode
        self._settings = {}  # type: Dict[str, Any]

        ## Signals
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo: Dict[int, object]) -> "SceneNode":
        copy = self.__class__()
        copy.setTransformation(self.getLocalTransformation())
        copy.setMeshData(self._mesh_data)
        copy._visible = cast(bool, deepcopy(self._visible, memo))
        copy._selectable = cast(bool, deepcopy(self._selectable, memo))
        copy._name = cast(str, deepcopy(self._name, memo))
        for decorator in self._decorators:
            copy.addDecorator(
                cast(SceneNodeDecorator, deepcopy(decorator, memo)))

        for child in self._children:
            copy.addChild(cast(SceneNode, deepcopy(child, memo)))
        self.calculateBoundingBoxMesh()
        return copy

    def setCenterPosition(self, center: Vector) -> None:
        """Set the center position of this node.

        This is used to modify it's mesh data (and it's children) in such a way that they are centered.
        In most cases this means that we use the center of mass as center (which most objects don't use)
        """

        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m).set(
                center_position=center)
        for child in self._children:
            child.setCenterPosition(center)

    def getParent(self) -> Optional["SceneNode"]:
        """Get the parent of this node.

        If the node has no parent, it is the root node.

        :returns: SceneNode if it has a parent and None if it's the root node.
        """

        return self._parent

    def getMirror(self) -> Vector:
        return self._mirror

    def setMirror(self, vector) -> None:
        self._mirror = vector

    def getBoundingBoxMesh(self) -> Optional[MeshData]:
        """Get the MeshData of the bounding box

        :returns: :type{MeshData} Bounding box mesh.
        """

        if self._bounding_box_mesh is None:
            self.calculateBoundingBoxMesh()
        return self._bounding_box_mesh

    def calculateBoundingBoxMesh(self) -> None:
        """(re)Calculate the bounding box mesh."""

        aabb = self.getBoundingBox()
        if aabb:
            bounding_box_mesh = MeshBuilder()
            rtf = aabb.maximum
            lbb = aabb.minimum

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back

            self._bounding_box_mesh = bounding_box_mesh.build()

    def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
        """Return if the provided bbox collides with the bbox of this SceneNode"""

        bbox = self.getBoundingBox()
        if bbox is not None:
            if check_bbox.intersectsBox(
                    bbox
            ) != AxisAlignedBox.IntersectionResult.FullIntersection:
                return True

        return False

    def _onParentChanged(self, node: Optional["SceneNode"]) -> None:
        """Handler for the ParentChanged signal
        :param node: Node from which this event was triggered.
        """

        for child in self.getChildren():
            child.parentChanged.emit(self)

    decoratorsChanged = Signal()
    """Signal for when a :type{SceneNodeDecorator} is added / removed."""

    def addDecorator(self, decorator: SceneNodeDecorator) -> None:
        """Add a SceneNodeDecorator to this SceneNode.

        :param decorator: The decorator to add.
        """

        if type(decorator) in [type(dec) for dec in self._decorators]:
            Logger.log(
                "w",
                "Unable to add the same decorator type (%s) to a SceneNode twice.",
                type(decorator))
            return
        try:
            decorator.setNode(self)
        except AttributeError:
            Logger.logException("e", "Unable to add decorator.")
            return
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    def getDecorators(self) -> List[SceneNodeDecorator]:
        """Get all SceneNodeDecorators that decorate this SceneNode.

        :return: list of all SceneNodeDecorators.
        """

        return self._decorators

    def getDecorator(self, dec_type: type) -> Optional[SceneNodeDecorator]:
        """Get SceneNodeDecorators by type.

        :param dec_type: type of decorator to return.
        """

        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator
        return None

    def removeDecorators(self):
        """Remove all decorators"""

        for decorator in self._decorators:
            decorator.clear()
        self._decorators = []
        self.decoratorsChanged.emit(self)

    def removeDecorator(self, dec_type: type) -> None:
        """Remove decorator by type.

        :param dec_type: type of the decorator to remove.
        """

        for decorator in self._decorators:
            if type(decorator) == dec_type:
                decorator.clear()
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    def callDecoration(self, function: str, *args, **kwargs) -> Any:
        """Call a decoration of this SceneNode.

        SceneNodeDecorators add Decorations, which are callable functions.
        :param function: The function to be called.
        :param *args
        :param **kwargs
        """

        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.logException("e",
                                        "Exception calling decoration %s: %s",
                                        str(function), str(e))
                    return None

    def hasDecoration(self, function: str) -> bool:
        """Does this SceneNode have a certain Decoration (as defined by a Decorator)
        :param :type{string} function the function to check for.
        """

        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self) -> str:
        return self._name

    def setName(self, name: str) -> None:
        self._name = name

    def getId(self) -> str:
        return self._id

    def setId(self, node_id: str) -> None:
        self._id = node_id

    def getDepth(self) -> int:
        """How many nodes is this node removed from the root?

        :return: Steps from root (0 means it -is- the root).
        """

        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    def setParent(self, scene_node: Optional["SceneNode"]) -> None:
        """:brief Set the parent of this object

        :param scene_node: SceneNode that is the parent of this object.
        """

        if self._parent:
            self._parent.removeChild(self)

        if scene_node:
            scene_node.addChild(self)

    parentChanged = Signal()
    """Emitted whenever the parent changes."""

    def isVisible(self) -> bool:
        """Get the visibility of this node.
        The parents visibility overrides the visibility.
        TODO: Let renderer actually use the visibility to decide whether to render or not.
        """

        if self._parent is not None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    def setVisible(self, visible: bool) -> None:
        """Set the visibility of this SceneNode."""

        self._visible = visible

    def getMeshData(self) -> Optional[MeshData]:
        """Get the (original) mesh data from the scene node/object.

        :returns: MeshData
        """

        return self._mesh_data

    def getMeshDataTransformed(self) -> Optional[MeshData]:
        """Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.

        If this node is a group, it will recursively concatenate all child nodes/objects.
        :returns: MeshData
        """

        return MeshData(vertices=self.getMeshDataTransformedVertices(),
                        normals=self.getMeshDataTransformedNormals())

    def getMeshDataTransformedVertices(self) -> numpy.ndarray:
        """Get the transformed vertices from this scene node/object, based on the transformation of scene nodes wrt root.

        If this node is a group, it will recursively concatenate all child nodes/objects.
        :return: numpy.ndarray
        """

        transformed_vertices = None
        if self.callDecoration("isGroup"):
            for child in self._children:
                tv = child.getMeshDataTransformedVertices()
                if transformed_vertices is None:
                    transformed_vertices = tv
                else:
                    transformed_vertices = numpy.concatenate(
                        (transformed_vertices, tv), axis=0)
        else:
            if self._mesh_data:
                transformed_vertices = self._mesh_data.getTransformed(
                    self.getWorldTransformation(copy=False)).getVertices()
        return transformed_vertices

    def getMeshDataTransformedNormals(self) -> numpy.ndarray:
        """Get the transformed normals from this scene node/object, based on the transformation of scene nodes wrt root.

        If this node is a group, it will recursively concatenate all child nodes/objects.
        :return: numpy.ndarray
        """

        transformed_normals = None
        if self.callDecoration("isGroup"):
            for child in self._children:
                tv = child.getMeshDataTransformedNormals()
                if transformed_normals is None:
                    transformed_normals = tv
                else:
                    transformed_normals = numpy.concatenate(
                        (transformed_normals, tv), axis=0)
        else:
            if self._mesh_data:
                transformed_normals = self._mesh_data.getTransformed(
                    self.getWorldTransformation(copy=False)).getNormals()
        return transformed_normals

    def setMeshData(self, mesh_data: Optional[MeshData]) -> None:
        """Set the mesh of this node/object

        :param mesh_data: MeshData object
        """

        self._mesh_data = mesh_data
        self._resetAABB()
        self.meshDataChanged.emit(self)

    meshDataChanged = Signal()
    """Emitted whenever the attached mesh data object changes."""

    def _onMeshDataChanged(self) -> None:
        self.meshDataChanged.emit(self)

    def addChild(self, scene_node: "SceneNode") -> None:
        """Add a child to this node and set it's parent as this node.

        :params scene_node SceneNode to add.
        """

        if scene_node in self._children:
            return

        scene_node.transformationChanged.connect(self.transformationChanged)
        scene_node.childrenChanged.connect(self.childrenChanged)
        scene_node.meshDataChanged.connect(self.meshDataChanged)

        self._children.append(scene_node)
        self._resetAABB()
        self.childrenChanged.emit(self)

        if not scene_node._parent is self:
            scene_node._parent = self
            scene_node._transformChanged()
            scene_node.parentChanged.emit(self)

    def removeChild(self, child: "SceneNode") -> None:
        """remove a single child

        :param child: Scene node that needs to be removed.
        """

        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self._resetAABB()
        self.childrenChanged.emit(self)

    def removeAllChildren(self) -> None:
        """Removes all children and its children's children."""

        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    def getChildren(self) -> List["SceneNode"]:
        """Get the list of direct children

        :returns: List of children
        """

        return self._children

    def hasChildren(self) -> bool:
        return True if self._children else False

    def getAllChildren(self) -> List["SceneNode"]:
        """Get list of all children (including it's children children children etc.)

        :returns: list ALl children in this 'tree'
        """

        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    childrenChanged = Signal()
    """Emitted whenever the list of children of this object or any child object changes.

    :param object: The object that triggered the change.
    """

    def _updateCachedNormalMatrix(self) -> None:
        self._cached_normal_matrix = Matrix(
            self.getWorldTransformation(copy=False).getData())
        self._cached_normal_matrix.setRow(3, [0, 0, 0, 1])
        self._cached_normal_matrix.setColumn(3, [0, 0, 0, 1])
        self._cached_normal_matrix.pseudoinvert()
        self._cached_normal_matrix.transpose()

    def getCachedNormalMatrix(self) -> Matrix:
        if self._cached_normal_matrix is None:
            self._updateCachedNormalMatrix()
        return self._cached_normal_matrix

    def getWorldTransformation(self, copy=True) -> Matrix:
        """Computes and returns the transformation from world to local space.

        :returns: 4x4 transformation matrix
        """

        if self._world_transformation is None:
            self._updateWorldTransformation()
        if copy:
            return self._world_transformation.copy()
        return self._world_transformation

    def getLocalTransformation(self, copy=True) -> Matrix:
        """Returns the local transformation with respect to its parent. (from parent to local)

        :returns transformation 4x4 (homogeneous) matrix
        """

        if self._transformation is None:
            self._updateLocalTransformation()
        if copy:
            return self._transformation.copy()
        return self._transformation

    def setTransformation(self, transformation: Matrix):
        self._transformation = transformation.copy(
        )  # Make a copy to ensure we never change the given transformation
        self._transformChanged()

    def getOrientation(self) -> Quaternion:
        """Get the local orientation value."""

        return deepcopy(self._orientation)

    def getWorldOrientation(self) -> Quaternion:
        return deepcopy(self._derived_orientation)

    def rotate(self,
               rotation: Quaternion,
               transform_space: int = TransformSpace.Local) -> None:
        """Rotate the scene object (and thus its children) by given amount

        :param rotation: :type{Quaternion} A quaternion indicating the amount of rotation.
        :param transform_space: The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
        """

        if not self._enabled:
            return

        orientation_matrix = rotation.toMatrix()
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(orientation_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    def setOrientation(self,
                       orientation: Quaternion,
                       transform_space: int = TransformSpace.Local) -> None:
        """Set the local orientation of this scene node.

        :param orientation: :type{Quaternion} The new orientation of this scene node.
        :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
        """

        if not self._enabled or orientation == self._orientation:
            return

        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldOrientation() == orientation:
                return
            new_orientation = orientation * (
                self.getWorldOrientation() *
                self._orientation.getInverse()).invert()
            orientation_matrix = new_orientation.toMatrix()
        else:  # Local
            orientation_matrix = orientation.toMatrix()

        euler_angles = orientation_matrix.getEuler()
        new_transform_matrix = Matrix()
        new_transform_matrix.compose(scale=self._scale,
                                     angles=euler_angles,
                                     translate=self._position,
                                     shear=self._shear)
        self._transformation = new_transform_matrix
        self._transformChanged()

    def getScale(self) -> Vector:
        """Get the local scaling value."""

        return self._scale

    def getWorldScale(self) -> Vector:
        return self._derived_scale

    def scale(self,
              scale: Vector,
              transform_space: int = TransformSpace.Local) -> None:
        """Scale the scene object (and thus its children) by given amount

        :param scale: :type{Vector} A Vector with three scale values
        :param transform_space: The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
        """

        if not self._enabled:
            return

        scale_matrix = Matrix()
        scale_matrix.setByScaleVector(scale)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(scale_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    def setScale(self,
                 scale: Vector,
                 transform_space: int = TransformSpace.Local) -> None:
        """Set the local scale value.

        :param scale: :type{Vector} The new scale value of the scene node.
        :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
        """

        if not self._enabled or scale == self._scale:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.scale(scale / self._scale, SceneNode.TransformSpace.Local)
            return
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldScale() == scale:
                return

            self.scale(scale / self._scale, SceneNode.TransformSpace.World)

    def getPosition(self) -> Vector:
        """Get the local position."""

        return self._position

    def getWorldPosition(self) -> Vector:
        """Get the position of this scene node relative to the world."""

        return self._derived_position

    def translate(self,
                  translation: Vector,
                  transform_space: int = TransformSpace.Local) -> None:
        """Translate the scene object (and thus its children) by given amount.

        :param translation: :type{Vector} The amount to translate by.
        :param transform_space: The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
        """

        if not self._enabled:
            return
        translation_matrix = Matrix()
        translation_matrix.setByTranslation(translation)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            world_transformation = self._world_transformation.copy()
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(translation_matrix)
            self._transformation.multiply(world_transformation)
        self._transformChanged()

    def setPosition(self,
                    position: Vector,
                    transform_space: int = TransformSpace.Local) -> None:
        """Set the local position value.

        :param position: The new position value of the SceneNode.
        :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
        """

        if not self._enabled or position == self._position:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.translate(position - self._position,
                           SceneNode.TransformSpace.Parent)
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldPosition() == position:
                return
            self.translate(position - self._derived_position,
                           SceneNode.TransformSpace.World)

    transformationChanged = Signal()
    """Signal. Emitted whenever the transformation of this object or any child object changes.
    :param object: The object that caused the change.
    """

    def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y) -> None:
        """Rotate this scene node in such a way that it is looking at target.

        :param target: :type{Vector} The target to look at.
        :param up: :type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
        """

        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalized()
        up = up.normalized()
        s = f.cross(up).normalized()
        u = s.cross(f).normalized()

        m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0],
                    [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]])

        self.setOrientation(Quaternion.fromMatrix(m))

    def render(self, renderer) -> bool:
        """Can be overridden by child nodes if they need to perform special rendering.
        If you need to handle rendering in a special way, for example for tool handles,
        you can override this method and render the node. Return True to prevent the
        view from rendering any attached mesh data.

        :param renderer: The renderer object to use for rendering.

        :return: False if the view should render this node, True if we handle our own rendering.
        """

        return False

    def isEnabled(self) -> bool:
        """Get whether this SceneNode is enabled, that is, it can be modified in any way."""

        if self._parent is not None and self._enabled:
            return self._parent.isEnabled()
        else:
            return self._enabled

    def setEnabled(self, enable: bool) -> None:
        """Set whether this SceneNode is enabled.

        :param enable: True if this object should be enabled, False if not.
        :sa isEnabled
        """

        self._enabled = enable

    def isSelectable(self) -> bool:
        """Get whether this SceneNode can be selected.

        :note This will return false if isEnabled() returns false.
        """

        return self._enabled and self._selectable

    def setSelectable(self, select: bool) -> None:
        """Set whether this SceneNode can be selected.

        :param select: True if this SceneNode should be selectable, False if not.
        """

        self._selectable = select

    def getBoundingBox(self) -> Optional[AxisAlignedBox]:
        """Get the bounding box of this node and its children."""

        if not self._calculate_aabb:
            return None
        if self._aabb is None:
            self._calculateAABB()
        return self._aabb

    def setCalculateBoundingBox(self, calculate: bool) -> None:
        """Set whether or not to calculate the bounding box for this node.

        :param calculate: True if the bounding box should be calculated, False if not.
        """

        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    def getShear(self) -> Vector:
        return self._shear

    def getSetting(self, key: str, default_value: str = "") -> str:
        return self._settings.get(key, default_value)

    def setSetting(self, key: str, value: str) -> None:
        self._settings[key] = value

    def invertNormals(self) -> None:
        for child in self._children:
            child.invertNormals()
        if self._mesh_data:
            self._mesh_data.invertNormals()

    def _transformChanged(self) -> None:
        self._updateTransformation()
        self._resetAABB()
        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateLocalTransformation(self) -> None:
        self._position, euler_angle_matrix, self._scale, self._shear = self._transformation.decompose(
        )

        self._orientation.setByMatrix(euler_angle_matrix)

    def _updateWorldTransformation(self) -> None:
        if self._parent:
            self._world_transformation = self._parent.getWorldTransformation(
            ).multiply(self._transformation)
        else:
            self._world_transformation = self._transformation

        self._derived_position, world_euler_angle_matrix, self._derived_scale, world_shear = self._world_transformation.decompose(
        )
        self._derived_orientation.setByMatrix(world_euler_angle_matrix)

    def _updateTransformation(self) -> None:
        self._updateLocalTransformation()
        self._updateWorldTransformation()
        self._updateCachedNormalMatrix()

    def _resetAABB(self) -> None:
        if not self._calculate_aabb:
            return
        self._aabb = None
        self._bounding_box_mesh = None
        if self._parent:
            self._parent._resetAABB()
        self.boundingBoxChanged.emit()

    def _calculateAABB(self) -> None:
        if self._mesh_data:
            aabb = self._mesh_data.getExtents(
                self.getWorldTransformation(copy=False))
        else:  # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
            position = self.getWorldPosition()
            aabb = AxisAlignedBox(minimum=position, maximum=position)

        for child in self._children:
            if aabb is None:
                aabb = child.getBoundingBox()
            else:
                aabb = aabb + child.getBoundingBox()
        self._aabb = aabb

    def __str__(self) -> str:
        """String output for debugging."""

        name = self._name if self._name != "" else hex(id(self))
        return "<" + self.__class__.__qualname__ + " object: '" + name + "'>"
예제 #14
0
class SceneNode():
    class TransformSpace:
        Local = 1
        Parent = 2
        World = 3

    ##  Construct a scene node.
    #   \param parent The parent of this node (if any). Only a root node should have None as a parent.
    #   \param kwargs Keyword arguments.
    #                 Possible keywords:
    #                 - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True
    #                 - name \type{string} Name of the SceneNode. Defaults to empty string.
    def __init__(self, parent=None, **kwargs):
        super().__init__()  # Call super to make multiple inheritance work.

        self._children = []  # type: List[SceneNode]
        self._mesh_data = None  # type: MeshData

        # Local transformation (from parent to local)
        self._transformation = Matrix()  # type: Matrix

        # Convenience "components" of the transformation
        self._position = Vector()  # type: Vector
        self._scale = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._shear = Vector(0.0, 0.0, 0.0)  # type: Vector
        self._mirror = Vector(1.0, 1.0, 1.0)  # type: Vector
        self._orientation = Quaternion()  # type: Quaternion

        # World transformation (from root to local)
        self._world_transformation = Matrix()  # type: Matrix

        # Convenience "components" of the world_transformation
        self._derived_position = Vector()  # type: Vector
        self._derived_orientation = Quaternion()  # type: Quaternion
        self._derived_scale = Vector()  # type: Vector

        self._parent = parent  # type: Optional[SceneNode]

        # Can this SceneNode be modified in any way?
        self._enabled = True  # type: bool
        # Can this SceneNode be selected in any way?
        self._selectable = False  # type: bool

        # Should the AxisAlignedBounxingBox be re-calculated?
        self._calculate_aabb = True  # type: bool

        # The AxisAligned bounding box.
        self._aabb = None  # type: Optional[AxisAlignedBox]
        self._bounding_box_mesh = None  # type: Optional[MeshData]

        self._visible = kwargs.get("visible", True)  # type: bool
        self._name = kwargs.get("name", "")  # type: str
        self._decorators = []  # type: List[SceneNodeDecorator]

        ## Signals
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo):
        copy = SceneNode()
        copy.setTransformation(self.getLocalTransformation())
        copy.setMeshData(self._mesh_data)
        copy.setVisible(deepcopy(self._visible, memo))
        copy._selectable = deepcopy(self._selectable, memo)
        copy._name = deepcopy(self._name, memo)
        for decorator in self._decorators:
            copy.addDecorator(deepcopy(decorator, memo))

        for child in self._children:
            copy.addChild(deepcopy(child, memo))
        self.calculateBoundingBoxMesh()
        return copy

    ##  Set the center position of this node.
    #   This is used to modify it's mesh data (and it's children) in such a way that they are centered.
    #   In most cases this means that we use the center of mass as center (which most objects don't use)
    def setCenterPosition(self, center: Vector):
        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m).set(
                center_position=center)
        for child in self._children:
            child.setCenterPosition(center)

    ##  \brief Get the parent of this node. If the node has no parent, it is the root node.
    #   \returns SceneNode if it has a parent and None if it's the root node.
    def getParent(self) -> Optional["SceneNode"]:
        return self._parent

    def getMirror(self) -> Vector:
        return self._mirror

    ##  Get the MeshData of the bounding box
    #   \returns \type{MeshData} Bounding box mesh.
    def getBoundingBoxMesh(self) -> Optional[MeshData]:
        return self._bounding_box_mesh

    ##  (re)Calculate the bounding box mesh.
    def calculateBoundingBoxMesh(self):
        aabb = self.getBoundingBox()
        if aabb:
            bounding_box_mesh = MeshBuilder()
            rtf = aabb.maximum
            lbb = aabb.minimum

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        rtf.z)  # Right - Top - Front
            bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                        lbb.z)  # Right - Top - Back

            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        rtf.z)  # Left - Top - Front
            bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                        lbb.z)  # Left - Top - Back

            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        rtf.z)  # Left - Bottom - Front
            bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                        lbb.z)  # Left - Bottom - Back

            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        rtf.z)  # Right - Bottom - Front
            bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                        lbb.z)  # Right - Bottom - Back

            self._bounding_box_mesh = bounding_box_mesh.build()

    ##  Handler for the ParentChanged signal
    #   \param node Node from which this event was triggered.
    def _onParentChanged(self, node: Optional["SceneNode"]):
        for child in self.getChildren():
            child.parentChanged.emit(self)

    ##  Signal for when a \type{SceneNodeDecorator} is added / removed.
    decoratorsChanged = Signal()

    ##  Add a SceneNodeDecorator to this SceneNode.
    #   \param \type{SceneNodeDecorator} decorator The decorator to add.
    def addDecorator(self, decorator: SceneNodeDecorator):
        if type(decorator) in [type(dec) for dec in self._decorators]:
            Logger.log(
                "w",
                "Unable to add the same decorator type (%s) to a SceneNode twice.",
                type(decorator))
            return
        try:
            decorator.setNode(self)
        except AttributeError:
            Logger.logException("e", "Unable to add decorator.")
            return
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    ##  Get all SceneNodeDecorators that decorate this SceneNode.
    #   \return list of all SceneNodeDecorators.
    def getDecorators(self) -> List[SceneNodeDecorator]:
        return self._decorators

    ##  Get SceneNodeDecorators by type.
    #   \param dec_type type of decorator to return.
    def getDecorator(self, dec_type) -> Optional[SceneNodeDecorator]:
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator

    ##  Remove all decorators
    def removeDecorators(self):
        for decorator in self._decorators:
            decorator.clear()
        self._decorators = []
        self.decoratorsChanged.emit(self)

    ##  Remove decorator by type.
    #   \param dec_type type of the decorator to remove.
    def removeDecorator(self, dec_type: SceneNodeDecorator):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                decorator.clear()
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    ##  Call a decoration of this SceneNode.
    #   SceneNodeDecorators add Decorations, which are callable functions.
    #   \param \type{string} function The function to be called.
    #   \param *args
    #   \param **kwargs
    def callDecoration(self, function: str, *args, **kwargs):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.log("e", "Exception calling decoration %s: %s",
                               str(function), str(e))
                    return None

    ##  Does this SceneNode have a certain Decoration (as defined by a Decorator)
    #   \param \type{string} function the function to check for.
    def hasDecoration(self, function: str) -> bool:
        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self) -> str:
        return self._name

    def setName(self, name: str):
        self._name = name

    ##  How many nodes is this node removed from the root?
    #   \return |tupe{int} Steps from root (0 means it -is- the root).
    def getDepth(self) -> int:
        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    ##  \brief Set the parent of this object
    #   \param scene_node SceneNode that is the parent of this object.
    def setParent(self, scene_node: Optional["SceneNode"]):
        if self._parent:
            self._parent.removeChild(self)

        if scene_node:
            scene_node.addChild(self)

    ##  Emitted whenever the parent changes.
    parentChanged = Signal()

    ##  \brief Get the visibility of this node. The parents visibility overrides the visibility.
    #   TODO: Let renderer actually use the visibility to decide whether to render or not.
    def isVisible(self) -> bool:
        if self._parent is not None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    ##  Set the visibility of this SceneNode.
    def setVisible(self, visible: bool):
        self._visible = visible

    ##  \brief Get the (original) mesh data from the scene node/object.
    #   \returns MeshData
    def getMeshData(self) -> Optional[MeshData]:
        return self._mesh_data

    ##  \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.
    #          If this node is a group, it will recursively concatenate all child nodes/objects.
    #   \returns MeshData
    def getMeshDataTransformed(self) -> Optional[MeshData]:
        return MeshData(vertices=self.getMeshDataTransformedVertices())

    ##  \brief Get the transformed vertices from this scene node/object, based on the transformation of scene nodes wrt root.
    #          If this node is a group, it will recursively concatenate all child nodes/objects.
    #   \return numpy.ndarray
    def getMeshDataTransformedVertices(self) -> numpy.ndarray:
        transformed_vertices = None
        if self.callDecoration("isGroup"):
            for child in self._children:
                tv = child.getMeshDataTransformedVertices()
                if transformed_vertices is None:
                    transformed_vertices = tv
                else:
                    transformed_vertices = numpy.concatenate(
                        (transformed_vertices, tv), axis=0)
        else:
            transformed_vertices = self._mesh_data.getTransformed(
                self.getWorldTransformation()).getVertices()
        return transformed_vertices

    ##  \brief Set the mesh of this node/object
    #   \param mesh_data MeshData object
    def setMeshData(self, mesh_data: Optional[MeshData]):
        self._mesh_data = mesh_data
        self._resetAABB()
        self.meshDataChanged.emit(self)

    ##  Emitted whenever the attached mesh data object changes.
    meshDataChanged = Signal()

    def _onMeshDataChanged(self):
        self.meshDataChanged.emit(self)

    ##  \brief Add a child to this node and set it's parent as this node.
    #   \params scene_node SceneNode to add.
    def addChild(self, scene_node: "SceneNode"):
        if scene_node not in self._children:
            scene_node.transformationChanged.connect(
                self.transformationChanged)
            scene_node.childrenChanged.connect(self.childrenChanged)
            scene_node.meshDataChanged.connect(self.meshDataChanged)

            self._children.append(scene_node)
            self._resetAABB()
            self.childrenChanged.emit(self)

            if not scene_node._parent is self:
                scene_node._parent = self
                scene_node._transformChanged()
                scene_node.parentChanged.emit(self)

    ##  \brief remove a single child
    #   \param child Scene node that needs to be removed.
    def removeChild(self, child: "SceneNode"):
        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self._resetAABB()
        self.childrenChanged.emit(self)

    ##  \brief Removes all children and its children's children.
    def removeAllChildren(self):
        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    ##  \brief Get the list of direct children
    #   \returns List of children
    def getChildren(self) -> List["SceneNode"]:
        return self._children

    def hasChildren(self) -> bool:
        return True if self._children else False

    ##  \brief Get list of all children (including it's children children children etc.)
    #   \returns list ALl children in this 'tree'
    def getAllChildren(self) -> List["SceneNode"]:
        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    ##  \brief Emitted whenever the list of children of this object or any child object changes.
    #   \param object The object that triggered the change.
    childrenChanged = Signal()

    ##  \brief Computes and returns the transformation from world to local space.
    #   \returns 4x4 transformation matrix
    def getWorldTransformation(self) -> Matrix:
        if self._world_transformation is None:
            self._updateTransformation()

        return deepcopy(self._world_transformation)

    ##  \brief Returns the local transformation with respect to its parent. (from parent to local)
    #   \retuns transformation 4x4 (homogenous) matrix
    def getLocalTransformation(self) -> Matrix:
        if self._transformation is None:
            self._updateTransformation()

        return deepcopy(self._transformation)

    def setTransformation(self, transformation: Matrix):
        self._transformation = deepcopy(
            transformation
        )  # Make a copy to ensure we never change the given transformation
        self._transformChanged()

    ##  Get the local orientation value.
    def getOrientation(self) -> Quaternion:
        return deepcopy(self._orientation)

    def getWorldOrientation(self) -> Quaternion:
        return deepcopy(self._derived_orientation)

    ##  \brief Rotate the scene object (and thus its children) by given amount
    #
    #   \param rotation \type{Quaternion} A quaternion indicating the amount of rotation.
    #   \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
    def rotate(self,
               rotation: Quaternion,
               transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return

        orientation_matrix = rotation.toMatrix()
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(orientation_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local orientation of this scene node.
    #
    #   \param orientation \type{Quaternion} The new orientation of this scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setOrientation(self,
                       orientation: Quaternion,
                       transform_space: int = TransformSpace.Local):
        if not self._enabled or orientation == self._orientation:
            return

        new_transform_matrix = Matrix()
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldOrientation() == orientation:
                return
            new_orientation = orientation * (
                self.getWorldOrientation() *
                self._orientation.getInverse()).getInverse()
            orientation_matrix = new_orientation.toMatrix()
        else:  # Local
            orientation_matrix = orientation.toMatrix()

        euler_angles = orientation_matrix.getEuler()

        new_transform_matrix.compose(scale=self._scale,
                                     angles=euler_angles,
                                     translate=self._position,
                                     shear=self._shear)
        self._transformation = new_transform_matrix
        self._transformChanged()

    ##  Get the local scaling value.
    def getScale(self) -> Vector:
        return self._scale

    def getWorldScale(self) -> Vector:
        return self._derived_scale

    ##  Scale the scene object (and thus its children) by given amount
    #
    #   \param scale \type{Vector} A Vector with three scale values
    #   \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
    def scale(self,
              scale: Vector,
              transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return

        scale_matrix = Matrix()
        scale_matrix.setByScaleVector(scale)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(scale_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local scale value.
    #
    #   \param scale \type{Vector} The new scale value of the scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setScale(self,
                 scale: Vector,
                 transform_space: int = TransformSpace.Local):
        if not self._enabled or scale == self._scale:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.scale(scale / self._scale, SceneNode.TransformSpace.Local)
            return
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldScale() == scale:
                return

            self.scale(scale / self._scale, SceneNode.TransformSpace.World)

    ##  Get the local position.
    def getPosition(self) -> Vector:
        return self._position

    ##  Get the position of this scene node relative to the world.
    def getWorldPosition(self) -> Vector:
        return self._derived_position

    ##  Translate the scene object (and thus its children) by given amount.
    #
    #   \param translation \type{Vector} The amount to translate by.
    #   \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
    def translate(self,
                  translation: Vector,
                  transform_space: int = TransformSpace.Local):
        if not self._enabled:
            return
        translation_matrix = Matrix()
        translation_matrix.setByTranslation(translation)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            world_transformation = deepcopy(self._world_transformation)
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(translation_matrix)
            self._transformation.multiply(world_transformation)
        self._transformChanged()

    ##  Set the local position value.
    #
    #   \param position The new position value of the SceneNode.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setPosition(self,
                    position: Vector,
                    transform_space: int = TransformSpace.Local):
        if not self._enabled or position == self._position:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.translate(position - self._position,
                           SceneNode.TransformSpace.Parent)
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldPosition() == position:
                return
            self.translate(position - self._derived_position,
                           SceneNode.TransformSpace.World)

    ##  Signal. Emitted whenever the transformation of this object or any child object changes.
    #   \param object The object that caused the change.
    transformationChanged = Signal()

    ##  Rotate this scene node in such a way that it is looking at target.
    #
    #   \param target \type{Vector} The target to look at.
    #   \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
    def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalized()
        up = up.normalized()
        s = f.cross(up).normalized()
        u = s.cross(f).normalized()

        m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0],
                    [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]])

        self.setOrientation(Quaternion.fromMatrix(m))

    ##  Can be overridden by child nodes if they need to perform special rendering.
    #   If you need to handle rendering in a special way, for example for tool handles,
    #   you can override this method and render the node. Return True to prevent the
    #   view from rendering any attached mesh data.
    #
    #   \param renderer The renderer object to use for rendering.
    #
    #   \return False if the view should render this node, True if we handle our own rendering.
    def render(self, renderer) -> bool:
        return False

    ##  Get whether this SceneNode is enabled, that is, it can be modified in any way.
    def isEnabled(self) -> bool:
        return self._enabled

    ##  Set whether this SceneNode is enabled.
    #   \param enable True if this object should be enabled, False if not.
    #   \sa isEnabled
    def setEnabled(self, enable: bool):
        self._enabled = enable

    ##  Get whether this SceneNode can be selected.
    #
    #   \note This will return false if isEnabled() returns false.
    def isSelectable(self) -> bool:
        return self._enabled and self._selectable

    ##  Set whether this SceneNode can be selected.
    #
    #   \param select True if this SceneNode should be selectable, False if not.
    def setSelectable(self, select: bool):
        self._selectable = select

    ##  Get the bounding box of this node and its children.
    def getBoundingBox(self) -> Optional[AxisAlignedBox]:
        if not self._calculate_aabb:
            return None
        if self._aabb is None:
            self._calculateAABB()
        return self._aabb

    ##  Set whether or not to calculate the bounding box for this node.
    #
    #   \param calculate True if the bounding box should be calculated, False if not.
    def setCalculateBoundingBox(self, calculate: bool):
        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    def getShear(self) -> Vector:
        return self._shear

    ##  private:
    def _transformChanged(self):
        self._updateTransformation()
        self._resetAABB()
        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateTransformation(self):
        scale, shear, euler_angles, translation, mirror = self._transformation.decompose(
        )
        self._position = translation
        self._scale = scale
        self._shear = shear
        self._mirror = mirror
        orientation = Quaternion()
        euler_angle_matrix = Matrix()
        euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y,
                                      euler_angles.z)
        orientation.setByMatrix(euler_angle_matrix)
        self._orientation = orientation
        if self._parent:
            self._world_transformation = self._parent.getWorldTransformation(
            ).multiply(self._transformation, copy=True)
        else:
            self._world_transformation = self._transformation

        world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose(
        )
        self._derived_position = world_translation
        self._derived_scale = world_scale

        world_euler_angle_matrix = Matrix()
        world_euler_angle_matrix.setByEuler(world_euler_angles.x,
                                            world_euler_angles.y,
                                            world_euler_angles.z)
        self._derived_orientation.setByMatrix(world_euler_angle_matrix)

    def _resetAABB(self):
        if not self._calculate_aabb:
            return
        self._aabb = None
        if self.getParent():
            self.getParent()._resetAABB()
        self.boundingBoxChanged.emit()

    def _calculateAABB(self):
        aabb = None
        if self._mesh_data:
            aabb = self._mesh_data.getExtents(self.getWorldTransformation())
        else:  # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
            position = self.getWorldPosition()
            aabb = AxisAlignedBox(minimum=position, maximum=position)

        for child in self._children:
            if aabb is None:
                aabb = child.getBoundingBox()
            else:
                aabb = aabb + child.getBoundingBox()
        self._aabb = aabb
예제 #15
0
class CliParser:
    progressChanged = Signal()
    timeMaterialEstimates = Signal()
    layersDataGenerated = Signal()

    def __init__(self, build_plate_number) -> None:
        self._profiled = False
        self._material_amounts = [0, 0]

        self._time_estimates = {
            "inset_0": 0,
            "inset_x": 0,
            "skin": 0,
            "infill": 0,
            "support_infill": 0,
            "support_interface": 0,
            "support": 0,
            "skirt": 0,
            "travel": 0,
            "retract": 0,
            "none": 0
        }

        self._build_plate_number = build_plate_number
        self._is_layers_in_file = False
        self._cancelled = False
        # self._message = None
        self._layer_number = -1
        self._extruder_number = 0
        self._pi_faction = 0
        self._position = Position
        self._gcode_position = Position
        # stack to get print settingd via getProperty method
        self._application = SteSlicerApplication.getInstance()
        self._global_stack = self._application.getGlobalContainerStack(
        )  # type: GlobalStack
        self._licensed = self._application.getLicenseManager().licenseValid

        self._rot_nwp = Matrix()
        self._rot_nws = Matrix()

        self._scene_node = None
        self._layer_type = LayerPolygon.Inset0Type
        self._extruder_number = 0
        # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._extruder_offsets = {}

        self._gcode_list = []

        self._current_layer_thickness = 0
        self._current_layer_height = 0

        # speeds
        self._travel_speed = 0
        self._wall_0_speed = 0
        self._skin_speed = 0
        self._infill_speed = 0
        self._support_speed = 0
        self._retraction_speed = 0
        self._prime_speed = 0

        # retraction
        self._enable_retraction = False
        self._retraction_amount = 0
        self._retraction_min_travel = 1.5
        self._retraction_hop_enabled = False
        self._retraction_hop = 1

        self._filament_diameter = 1.75
        self._line_width = 0.4
        self._layer_thickness = 0.2
        self._clearValues()

    _layer_keyword = "$$LAYER/"
    _geometry_end_keyword = "$$GEOMETRYEND"
    _body_type_keyword = "//body//"
    _support_type_keyword = "//support//"
    _skin_type_keyword = "//skin//"
    _infill_type_keyword = "//infill//"
    _perimeter_type_keyword = "//perimeter//"

    _type_keyword = ";TYPE:"

    def cancel(self):
        self._cancelled = True

    def getLayersData(self):
        if self._layer_data_builder:
            return self._layer_data_builder.getLayers()
        else:
            return []

    def getMaterialAmounts(self):
        return self._material_amounts

    def getTimes(self):
        return self._time_estimates

    def processCliStream(self, stream: str) -> List[str]:
        Logger.log("d", "Preparing to load CLI")
        start_time = time()
        self._cancelled = False
        self._setPrintSettings()
        self._is_layers_in_file = False

        self._gcode_list = []

        self._writeStartCode(self._gcode_list)
        self._gcode_list[-1] += ";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.progressChanged.emit(0)

        Logger.log("d", "Parsing CLI...")

        self._position = Position(0, 0, 0, 0, 0, 1, 0, [0])
        self._gcode_position = Position(999, 999, 999, 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.progressChanged.emit(
                    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
                    self._addToPath(current_path, [
                        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
                    ])
                    # 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])
                    if not (self._gcode_list[-1].startswith(";LAYER:")
                            and self._gcode_list[-1].count('\n') < 2):
                        self._layer_number += 1
                        self._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._gcode_list[-1] = self.processPolyline(
                line, current_path, self._gcode_list[-1])

            if self._cancelled:
                return None

        # "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()

        self._gcode_list[0].replace(";LAYER_COUNT:\n",
                                    ";LAYER_COUNT:%s\n" % self._layer_number)

        end_gcode = self._global_stack.getProperty("machine_end_gcode",
                                                   "value")
        self._gcode_list.append(end_gcode + "\n")

        self.timeMaterialEstimates.emit(self._material_amounts,
                                        self._time_estimates)
        self.layersDataGenerated.emit(self._layer_data_builder.getLayers())

        return self._gcode_list

    def _setPrintSettings(self):
        pass

    def _onHideMessage(self, message: Optional[Union[str, Message]]) -> None:
        if message == self._message:
            self._cancelled = True

    def _clearValues(self):
        self._material_amounts = [0.0, 0.0]
        self._time_estimates = {
            "inset_0": 60,
            "inset_x": 0,
            "skin": 0,
            "infill": 0,
            "support_infill": 0,
            "support_interface": 0,
            "support": 0,
            "skirt": 0,
            "travel": 0,
            "retract": 0,
            "none": 0
        }
        self._extruder_number = 0
        self._layer_number = -1
        self._layer_data_builder = LayerDataBuilder()
        self._pi_faction = 0
        self._position = Position(0, 0, 0, 0, 0, 1, 0, [0])
        self._gcode_position = Position(0, 0, 0, 0, 0, 0, 0, [0])
        self._rot_nwp = Matrix()
        self._rot_nws = Matrix()
        self._layer_type = LayerPolygon.Inset0Type

        self._parsing_type = self._global_stack.getProperty(
            "printing_mode", "value")
        self._line_width = self._global_stack.getProperty(
            "wall_line_width_0", "value")
        self._layer_thickness = self._global_stack.getProperty(
            "layer_height", "value")

        self._travel_speed = self._global_stack.getProperty(
            "speed_travel", "value")
        self._wall_0_speed = self._global_stack.getProperty(
            "speed_wall_0", "value")
        self._skin_speed = self._global_stack.getProperty(
            "speed_topbottom", "value")
        self._infill_speed = self._global_stack.getProperty(
            "speed_infill", "value")
        self._support_speed = self._global_stack.getProperty(
            "speed_support", "value")
        self._retraction_speed = self._global_stack.getProperty(
            "retraction_retract_speed", "value")
        self._prime_speed = self._global_stack.getProperty(
            "retraction_prime_speed", "value")

        extruder = self._global_stack.extruders.get(
            "%s" % self._extruder_number,
            None)  # type: Optional[ExtruderStack]

        self._filament_diameter = extruder.getProperty("material_diameter",
                                                       "value")
        self._enable_retraction = extruder.getProperty("retraction_enable",
                                                       "value")
        self._retraction_amount = extruder.getProperty("retraction_amount",
                                                       "value")
        self._retraction_min_travel = extruder.getProperty(
            "retraction_min_travel", "value")
        self._retraction_hop_enabled = extruder.getProperty(
            "retraction_hop_enabled", "value")
        self._retraction_hop = extruder.getProperty("retraction_hop", "value")

    def _setByRotationAxis(self,
                           matrix,
                           angle: float,
                           direction: Vector,
                           point: Optional[List[float]] = None) -> None:
        sina = math.sin(angle)
        cosa = math.cos(angle)
        direction_data = matrix._unitVector(direction.getData())
        # rotation matrix around unit vector
        R = numpy.diag([cosa, cosa, cosa])
        R += numpy.outer(direction_data, direction_data) * (1.0 - cosa)
        direction_data *= sina
        R += numpy.array([[0.0, -direction_data[2], direction_data[1]],
                          [direction_data[2], 0.0, -direction_data[0]],
                          [-direction_data[1], direction_data[0], 0.0]],
                         dtype=numpy.float64)
        M = numpy.identity(4)
        M[:3, :3] = R
        if point is not None:
            # rotation not around origin
            point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
            M[:3, 3] = point - numpy.dot(R, point)
        matrix._data = M

    def _transformCoordinates(
            self, x: float, y: float, z: float, i: float, j: float, k: float,
            position: Position) -> (float, float, float, float, float, float):
        a = position.a
        c = position.c
        # Get coordinate angles
        if abs(self._position.c - k) > 0.00001:
            a = numpy.arccos(k)
            self._rot_nwp = Matrix()
            self._setByRotationAxis(self._rot_nwp, -a, Vector.Unit_X)
            # self._rot_nwp.setByRotationAxis(-a, Vector.Unit_X)
            a = numpy.degrees(a)
        if abs(self._position.a - i) > 0.00001 or abs(self._position.b -
                                                      j) > 0.00001:
            c = numpy.arctan2(j, i) if x != 0 and y != 0 else 0
            angle = numpy.degrees(c + self._pi_faction * 2 * numpy.pi)
            if abs(angle - position.c) > 180:
                self._pi_faction += 1 if (angle - position.c) < 0 else -1
            c += self._pi_faction * 2 * numpy.pi
            c -= numpy.pi / 2
            self._rot_nws = Matrix()
            self._setByRotationAxis(self._rot_nws, c, Vector.Unit_Z)
            # self._rot_nws.setByRotationAxis(c, Vector.Unit_Z)
            c = numpy.degrees(c)

        tr = self._rot_nws.multiply(self._rot_nwp, True)
        tr.invert()
        pt = Vector(data=numpy.array([x, y, z, 1]))
        ret = tr.multiply(pt, True).getData()

        return Position(ret[0], ret[1], ret[2], a, 0, c, 0, [0])

    @staticmethod
    def _getValue(line: str, key: str) -> Optional[str]:
        n = line.find(key)
        if n < 0:
            return None
        n += len(key)
        splitted = line[n:].split("/")
        if len(splitted) > 1:
            return splitted[1]
        else:
            return None

    def _createPolygon(self, layer_thickness: float,
                       path: List[List[Union[float, int]]],
                       extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[8] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number,
                                                    self._current_layer_height)
            self._layer_data_builder.setLayerThickness(self._layer_number,
                                                       layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 6), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:

            points[i, :] = [
                point[0] + extruder_offsets[0], point[2],
                -point[1] - extruder_offsets[1], -point[4], point[5], -point[3]
            ]
            extrusion_values[i] = point[7]
            if i > 0:
                line_feedrates[i - 1] = point[6]
                line_types[i - 1] = point[8]
                if point[8] in [
                        LayerPolygon.MoveCombingType,
                        LayerPolygon.MoveRetractionType
                ]:
                    line_widths[i - 1] = 0.1
                    # Travels are set as zero thickness lines
                    line_thicknesses[i - 1] = 0.0
                else:
                    line_widths[i - 1] = self._line_width
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points,
                                 line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def processPolyline(self, line: str, path: List[List[Union[float, int]]],
                        gcode_line: str) -> str:
        # Convering line to point array
        values_line = self._getValue(line, "$$POLYLINE")
        if not values_line:
            return gcode_line
        values = values_line.split(",")
        if len(values[3:]) % 2 != 0:
            return gcode_line
        idx = 2
        points = values[3:]
        if len(points) < 2:
            return gcode_line
        # TODO: add combing to this polyline
        new_position, new_gcode_position = self._cliPointToPosition(
            CliPoint(float(points[0]), float(points[1])), self._position,
            False)

        is_retraction = self._enable_retraction and self._positionLength(
            self._position, new_position
        ) > self._retraction_min_travel and self._layer_type not in [
            LayerPolygon.InfillType, LayerPolygon.SupportType
        ]
        if is_retraction:
            # we have retraction move
            new_extruder_position = self._position.e[
                self._extruder_number] - self._retraction_amount
            gcode_line += "G1 E%.5f F%.0f\n" % (new_extruder_position,
                                                (self._retraction_speed * 60))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[
                self._extruder_number] = new_extruder_position
            self._addToPath(path, [
                self._position.x, self._position.y, self._position.z,
                self._position.a, self._position.b, self._position.c,
                self._retraction_speed, self._position.e,
                LayerPolygon.MoveRetractionType
            ])
            # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
            #             self._position.c, self._retraction_speed, self._position.e, LayerPolygon.MoveRetractionType])

            if self._retraction_hop_enabled:
                # add hop movement
                gx, gy, gz, ga, gb, gc, gf, ge = self._gcode_position
                x, y, z, a, b, c, f, e = self._position
                gcode_position = Position(gx, gy, gz + self._retraction_hop,
                                          ga, gb, gc, self._travel_speed, ge)
                self._position = Position(x + a * self._retraction_hop,
                                          y + b * self._retraction_hop,
                                          z + c * self._retraction_hop, a, b,
                                          c, self._travel_speed, e)
                gcode_command = self._generateGCodeCommand(
                    0, gcode_position, self._travel_speed)
                if gcode_command is not None:
                    gcode_line += gcode_command
                self._gcode_position = gcode_position
                self._addToPath(path, [
                    self._position.x, self._position.y, self._position.z,
                    self._position.a, self._position.b, self._position.c,
                    self._prime_speed, self._position.e,
                    LayerPolygon.MoveCombingType
                ])
                # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                #             self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveCombingType])
                gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
                x, y, z, a, b, c, f, e = new_position
                gcode_position = Position(gx, gy, gz + self._retraction_hop,
                                          ga, gb, gc, self._travel_speed, ge)
                position = Position(x + a * self._retraction_hop,
                                    y + b * self._retraction_hop,
                                    z + c * self._retraction_hop, a, b, c,
                                    self._travel_speed, e)
                gcode_command = self._generateGCodeCommand(
                    0, gcode_position, self._travel_speed)
                if gcode_command is not None:
                    gcode_line += gcode_command
                self._addToPath(path, [
                    position.x, position.y, position.z, position.a, position.b,
                    position.c, position.f, position.e,
                    LayerPolygon.MoveCombingType
                ])
                # path.append([position.x, position.y, position.z, position.a, position.b,
                #             position.c, position.f, position.e, LayerPolygon.MoveCombingType])

        feedrate = self._travel_speed
        x, y, z, a, b, c, f, e = new_position
        self._position = Position(x, y, z, a, b, c, feedrate, self._position.e)
        gcode_command = self._generateGCodeCommand(0, new_gcode_position,
                                                   feedrate)
        if gcode_command is not None:
            gcode_line += gcode_command
        gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
        self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate, ge)
        self._addToPath(
            path,
            [x, y, z, a, b, c, feedrate, e, LayerPolygon.MoveCombingType])
        # path.append([x, y, z, a, b, c, feedrate, e,
        #             LayerPolygon.MoveCombingType])

        if is_retraction:
            # we have retraction move
            new_extruder_position = self._position.e[
                self._extruder_number] + self._retraction_amount
            gcode_line += "G1 E%.5f F%.0f\n" % (new_extruder_position,
                                                (self._prime_speed * 60))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[
                self._extruder_number] = new_extruder_position
            self._addToPath(path, [
                self._position.x, self._position.y, self._position.z,
                self._position.a, self._position.b, self._position.c,
                self._prime_speed, self._position.e,
                LayerPolygon.MoveRetractionType
            ])
            # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
            #             self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveRetractionType])

        if self._layer_type == LayerPolygon.SupportType:
            gcode_line += self._type_keyword + "SUPPORT\n"
        elif self._layer_type == LayerPolygon.SkinType:
            gcode_line += self._type_keyword + "SKIN\n"
        elif self._layer_type == LayerPolygon.InfillType:
            gcode_line += self._type_keyword + "FILL\n"
        else:
            gcode_line += self._type_keyword + "WALL-OUTER\n"
        gcode_position_list = []
        last_gcode_position = self._gcode_position
        while idx < len(points):
            point = CliPoint(float(points[idx]), float(points[idx + 1]))
            idx += 2
            new_position, new_gcode_position = self._cliPointToPosition(
                point, self._position)
            feedrate = self._wall_0_speed
            if self._layer_type == LayerPolygon.SupportType:
                feedrate = self._support_speed
            elif self._layer_type == LayerPolygon.SkinType:
                feedrate = self._skin_speed
            elif self._layer_type == LayerPolygon.InfillType:
                feedrate = self._infill_speed
            x, y, z, a, b, c, f, e = new_position
            self._position = Position(x, y, z, a, b, c, feedrate, e)
            #gcode_command = self._generateGCodeCommand(1, new_gcode_position, feedrate)
            #if gcode_command is not None:
            #    gcode_line += gcode_command
            gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
            self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate,
                                            ge)
            gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate, ge)
            gcode_position_list.append(gcode_position)
            self._addToPath(path,
                            [x, y, z, a, b, c, feedrate, e, self._layer_type])
            # path.append([x, y, z, a, b, c, feedrate, e, self._layer_type])
        self._gcode_position = last_gcode_position
        if len(gcode_position_list) > 0:
            filtered_gcode_position_list = [gcode_position_list[0]]
            for index in range(1, len(gcode_position_list) - 2):
                dist2 = self.getDist2FromLineSegment(
                    gcode_position_list[index - 1], gcode_position_list[index],
                    gcode_position_list[index + 1])
                if dist2 > 0.000001:
                    filtered_gcode_position_list.append(
                        gcode_position_list[index])
            filtered_gcode_position_list.append(gcode_position_list[-1])
            for gcode_position in filtered_gcode_position_list:
                gcode_command = self._generateGCodeCommand(
                    1, gcode_position, gcode_position.f)
                if gcode_command is not None:
                    gcode_line += gcode_command
                gx, gy, gz, ga, gb, gc, gf, ge = gcode_position
                self._gcode_position = Position(gx, gy, gz, ga, gb, gc, gf, ge)
        return gcode_line

    def _generateGCodeCommand(self, g: int, gcode_position: Position,
                              feedrate: float) -> Optional[str]:
        gcode_command = "G%s" % g
        if numpy.abs(gcode_position.x - self._gcode_position.x) > 0.0001:
            gcode_command += " X%.2f" % gcode_position.x
        if numpy.abs(gcode_position.y - self._gcode_position.y) > 0.0001:
            gcode_command += " Y%.2f" % gcode_position.y
        if numpy.abs(gcode_position.z - self._gcode_position.z) > 0.0001:
            gcode_command += " Z%.2f" % gcode_position.z
        if numpy.abs(gcode_position.a - self._gcode_position.a) > 0.0001:
            gcode_command += " A%.2f" % gcode_position.a
        if numpy.abs(gcode_position.b - self._gcode_position.b) > 0.0001:
            gcode_command += " B%.2f" % gcode_position.b
        if numpy.abs(gcode_position.c - self._gcode_position.c) > 0.0001:
            gcode_command += " C%.3f" % (gcode_position.c / 3)
        if numpy.abs(feedrate - self._gcode_position.f) > 0.0001:
            gcode_command += " F%.0f" % (feedrate * 60)
        if numpy.abs(gcode_position.e[self._extruder_number] -
                     self._gcode_position.e[self._extruder_number]
                     ) > 0.0001 and g > 0:
            gcode_command += " E%.5f" % gcode_position.e[self._extruder_number]
        gcode_command += "\n"
        if gcode_command != "G%s\n" % g:
            return gcode_command
        else:
            return None

    def _calculateExtrusion(self, current_point: List[float],
                            previous_point: Position) -> float:

        Af = (self._filament_diameter / 2)**2 * 3.14
        Al = self._line_width * self._layer_thickness
        de = numpy.sqrt((current_point[0] - previous_point[0])**2 +
                        (current_point[1] - previous_point[1])**2 +
                        (current_point[2] - previous_point[2])**2)
        dVe = Al * de
        self._material_amounts[self._extruder_number] += float(dVe)
        return dVe / Af

    def _writeStartCode(self, gcode_list: List[str]):
        if self._parsing_type == "cylindrical":
            start_gcode = "T0\n"
            extruder = self._global_stack.extruders.get(
                "%s" % self._extruder_number,
                None)  # type: Optional[ExtruderStack]
            init_temperature = extruder.getProperty(
                "material_print_temperature", "value")
            init_bed_temperature = extruder.getProperty(
                "material_bed_temperature", "value")
            has_heated_bed = self._global_stack.getProperty(
                "machine_heated_bed", "value")
            if has_heated_bed:
                start_gcode += "M140 S%s\n" % init_bed_temperature
                start_gcode += "M105\n"
                start_gcode += "M190 S%s\n" % init_bed_temperature
            start_gcode += "M104 S%s\n" % init_temperature
            start_gcode += "M105\n"
            start_gcode += "M109 S%s\n" % init_temperature
            start_gcode += "M82 ;absolute extrusion mode\n"
            start_gcode_prefix = self._global_stack.getProperty(
                "machine_start_gcode", "value")
            if self._parsing_type in ["cylindrical", "cylindrical_full"]:
                start_gcode_prefix = start_gcode_prefix.replace("G55", "G56")
            start_gcode += start_gcode_prefix
            gcode_list.append(start_gcode + "\n")
        elif self._parsing_type == "cylindrical_full":
            gcode_list.append(
                "G91\nG0 Z20\nG90\nG54\nG0 Z125 A90 F600\nG92 E0 C0\nG1 F200 E-1 ;retract 1 mm of feed stock\nG92 E0 ;zero the extruded length again\nG56\nG1 F200 E1 ;extrude 1 mm of feed stock\nG92 E0 ;zero the extruded length again\n"
            )
        else:
            gcode_list.append(
                "G54\nG0 Z125 A90 F600\nG92 E0 C0\nG1 F200 E6 ;extrude 6 mm of feed stock\nG92 E0 ;zero the extruded length again\nG56\n"
            )

    def _cliPointToPosition(
            self,
            point: CliPoint,
            position: Position,
            extrusion_move: bool = True) -> (Position, Position):
        x, y, z, i, j, k = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
        if self._parsing_type == "classic":
            x = point.x
            y = point.y
            z = self._current_layer_height
            i = 0
            j = 0
            k = 1
        elif self._parsing_type in ["cylindrical", "cylindrical_full"]:
            x = self._current_layer_height * numpy.cos(point.y)
            y = self._current_layer_height * numpy.sin(point.y)
            z = point.x
            length = numpy.sqrt(x**2 + y**2)
            i = x / length if length != 0 else 0
            j = y / length if length != 0 else 0
            k = 0
        new_position = Position(x, y, z, i, j, k, 0, [0])

        new_gcode_position = self._transformCoordinates(
            x, y, z, i, j, k, self._gcode_position)
        new_position.e[self._extruder_number] = position.e[self._extruder_number] + self._calculateExtrusion([x, y, z],
                                                                                                             position) if extrusion_move else \
            position.e[self._extruder_number]
        new_gcode_position.e[self._extruder_number] = new_position.e[
            self._extruder_number]

        return new_position, new_gcode_position

    @staticmethod
    def _positionLength(start: Position, end: Position) -> float:
        return numpy.sqrt((start.x - end.x)**2 + (start.y - end.y)**2 +
                          (start.z - end.z)**2)

    def _addToPath(self, path: List[List[Union[float, int]]],
                   addition: List[Union[float, int]]):
        layer_type = addition[8]
        layer_type_to_times_type = {
            LayerPolygon.NoneType: "none",
            LayerPolygon.Inset0Type: "inset_0",
            LayerPolygon.InsetXType: "inset_x",
            LayerPolygon.SkinType: "skin",
            LayerPolygon.SupportType: "support",
            LayerPolygon.SkirtType: "skirt",
            LayerPolygon.InfillType: "infill",
            LayerPolygon.SupportInfillType: "support_infill",
            LayerPolygon.MoveCombingType: "travel",
            LayerPolygon.MoveRetractionType: "retract",
            LayerPolygon.SupportInterfaceType: "support_interface"
        }
        if len(path) > 0:
            last_point = path[-1]
        else:
            last_point = addition
        length = numpy.sqrt((last_point[0] - addition[0])**2 +
                            (last_point[1] - addition[1])**2 +
                            (last_point[2] - addition[2])**2)
        feedrate = addition[6]
        if feedrate == 0:
            feedrate = self._travel_speed
        self._time_estimates[
            layer_type_to_times_type[layer_type]] += (length / feedrate) * 2
        path.append(addition)

    @staticmethod
    def getDist2FromLineSegment(a: Position, b: Position,
                                c: Position) -> float:
        c_arr = [c.x, c.y, c.z, c.a, c.b, c.c]
        a_arr = [a.x, a.y, a.z, a.a, a.b, a.c]
        b_arr = [b.x, b.y, b.z, b.a, b.b, b.c]
        ac = numpy.subtract(c_arr, a_arr)
        ac_size = numpy.sqrt(numpy.sum(ac**2))
        ab = numpy.subtract(b_arr, a_arr)
        if ac_size == 0:
            ab_dist = numpy.sum(ab**2)
            return ab_dist
        projected_x = numpy.dot(ab, ac)
        ax_size = projected_x / ac_size
        if ax_size < 0:
            return numpy.sum(ab**2)
        if (ax_size > ac_size):
            return numpy.sum(numpy.subtract(b_arr, c_arr)**2)
        ax = ac * ax_size / ac_size
        bx = ab - ax
        bx_size = numpy.sum(bx**2)
        return bx_size
예제 #16
0
class SceneNode(SignalEmitter):
    class TransformSpace:
        Local = 1
        Parent = 2
        World = 3

    def __init__(self, parent=None, **kwargs):
        super().__init__()  # Call super to make multiple inheritence work.

        self._children = []
        self._mesh_data = None

        self._position = Vector()
        self._scale = Vector(1.0, 1.0, 1.0)
        self._shear = Vector(0.0, 0.0, 0.0)
        self._orientation = Quaternion()

        self._transformation = Matrix()  #local transformation
        self._world_transformation = Matrix()

        self._derived_position = Vector()
        self._derived_orientation = Quaternion()
        self._derived_scale = Vector()

        self._inherit_orientation = True
        self._inherit_scale = True

        self._parent = parent
        self._enabled = True
        self._selectable = False
        self._calculate_aabb = True
        self._aabb = None
        self._aabb_job = None
        self._visible = kwargs.get("visible", True)
        self._name = kwargs.get("name", "")
        self._decorators = []
        self._bounding_box_mesh = None
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo):
        copy = SceneNode()
        copy.translate(self.getPosition())
        copy.setOrientation(self.getOrientation())
        copy.setScale(self.getScale())
        copy.setMeshData(deepcopy(self._mesh_data, memo))
        copy.setVisible(deepcopy(self._visible, memo))
        copy._selectable = deepcopy(self._selectable, memo)
        for decorator in self._decorators:
            copy.addDecorator(deepcopy(decorator, memo))

        for child in self._children:
            copy.addChild(deepcopy(child, memo))
        self.calculateBoundingBoxMesh()
        return copy

    def setCenterPosition(self, center):
        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m)
            self._mesh_data.setCenterPosition(center)
        for child in self._children:
            child.setCenterPosition(center)

    ##  \brief Get the parent of this node. If the node has no parent, it is the root node.
    #   \returns SceneNode if it has a parent and None if it's the root node.
    def getParent(self):
        return self._parent

    def getBoundingBoxMesh(self):
        return self._bounding_box_mesh

    def calculateBoundingBoxMesh(self):
        if self._aabb:
            self._bounding_box_mesh = MeshData()
            rtf = self._aabb.maximum
            lbb = self._aabb.minimum

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              rtf.z)  #Right - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              rtf.z)  #Left - Top - Front

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              rtf.z)  #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              rtf.z)  #Left - Bottom - Front

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              rtf.z)  #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              rtf.z)  #Right - Bottom - Front

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              rtf.z)  #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              rtf.z)  #Right - Top - Front

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              lbb.z)  #Right - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              lbb.z)  #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              lbb.z)  #Left - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              lbb.z)  #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              lbb.z)  #Left - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              lbb.z)  #Right - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              lbb.z)  #Right - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              lbb.z)  #Right - Top - Back

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              rtf.z)  #Right - Top - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y,
                                              lbb.z)  #Right - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              rtf.z)  #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y,
                                              lbb.z)  #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              rtf.z)  #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y,
                                              lbb.z)  #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              rtf.z)  #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y,
                                              lbb.z)  #Right - Bottom - Back
        else:
            self._resetAABB()

    def _onParentChanged(self, node):
        for child in self.getChildren():
            child.parentChanged.emit(self)

    decoratorsChanged = Signal()

    def addDecorator(self, decorator):
        decorator.setNode(self)
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    def getDecorators(self):
        return self._decorators

    def getDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator

    def removeDecorators(self):
        self._decorators = []
        self.decoratorsChanged.emit(self)

    def removeDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    def callDecoration(self, function, *args, **kwargs):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.log("e", "Exception calling decoration %s: %s",
                               str(function), str(e))
                    return None

    def hasDecoration(self, function):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self):
        return self._name

    def setName(self, name):
        self._name = name

    ##  How many nodes is this node removed from the root
    def getDepth(self):
        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    ##  \brief Set the parent of this object
    #   \param scene_node SceneNode that is the parent of this object.
    def setParent(self, scene_node):
        if self._parent:
            self._parent.removeChild(self)
        #self._parent = scene_node

        if scene_node:
            scene_node.addChild(self)

    ##  Emitted whenever the parent changes.
    parentChanged = Signal()

    ##  \brief Get the visibility of this node. The parents visibility overrides the visibility.
    #   TODO: Let renderer actually use the visibility to decide wether to render or not.
    def isVisible(self):
        if self._parent != None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    def setVisible(self, visible):
        self._visible = visible

    ##  \brief Get the (original) mesh data from the scene node/object.
    #   \returns MeshData
    def getMeshData(self):
        return self._mesh_data

    ##  \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.
    #   \returns MeshData
    def getMeshDataTransformed(self):
        #transformed_mesh = deepcopy(self._mesh_data)
        #transformed_mesh.transform(self.getWorldTransformation())
        return self._mesh_data.getTransformed(self.getWorldTransformation())

    ##  \brief Set the mesh of this node/object
    #   \param mesh_data MeshData object
    def setMeshData(self, mesh_data):
        if self._mesh_data:
            self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged)
        self._mesh_data = mesh_data
        if self._mesh_data is not None:
            self._mesh_data.dataChanged.connect(self._onMeshDataChanged)
        self._resetAABB()
        self.meshDataChanged.emit(self)

    ##  Emitted whenever the attached mesh data object changes.
    meshDataChanged = Signal()

    def _onMeshDataChanged(self):
        self.meshDataChanged.emit(self)

    ##  \brief Add a child to this node and set it's parent as this node.
    #   \params scene_node SceneNode to add.
    def addChild(self, scene_node):
        if scene_node not in self._children:
            scene_node.transformationChanged.connect(
                self.transformationChanged)
            scene_node.childrenChanged.connect(self.childrenChanged)
            scene_node.meshDataChanged.connect(self.meshDataChanged)

            self._children.append(scene_node)
            self._resetAABB()
            self.childrenChanged.emit(self)

            if not scene_node._parent is self:
                scene_node._parent = self
                scene_node._transformChanged()
                scene_node.parentChanged.emit(self)

    ##  \brief remove a single child
    #   \param child Scene node that needs to be removed.
    def removeChild(self, child):
        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self.childrenChanged.emit(self)

    ##  \brief Removes all children and its children's children.
    def removeAllChildren(self):
        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    ##  \brief Get the list of direct children
    #   \returns List of children
    def getChildren(self):
        return self._children

    def hasChildren(self):
        return True if self._children else False

    ##  \brief Get list of all children (including it's children children children etc.)
    #   \returns list ALl children in this 'tree'
    def getAllChildren(self):
        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    ##  \brief Emitted whenever the list of children of this object or any child object changes.
    #   \param object The object that triggered the change.
    childrenChanged = Signal()

    ##  \brief Computes and returns the transformation from world to local space.
    #   \returns 4x4 transformation matrix
    def getWorldTransformation(self):
        if self._world_transformation is None:
            self._updateTransformation()

        return deepcopy(self._world_transformation)

    ##  \brief Returns the local transformation with respect to its parent. (from parent to local)
    #   \retuns transformation 4x4 (homogenous) matrix
    def getLocalTransformation(self):
        if self._transformation is None:
            self._updateTransformation()

        return deepcopy(self._transformation)

    def setTransformation(self, transformation):
        self._transformation = transformation
        self._transformChanged()

    ##  Get the local orientation value.
    def getOrientation(self):
        return deepcopy(self._orientation)

    def getWorldOrientation(self):
        return deepcopy(self._derived_orientation)

    ##  \brief Rotate the scene object (and thus its children) by given amount
    #
    #   \param rotation \type{Quaternion} A quaternion indicating the amount of rotation.
    #   \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
    def rotate(self, rotation, transform_space=TransformSpace.Local):
        if not self._enabled:
            return

        orientation_matrix = rotation.toMatrix()
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(orientation_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local orientation of this scene node.
    #
    #   \param orientation \type{Quaternion} The new orientation of this scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setOrientation(self,
                       orientation,
                       transform_space=TransformSpace.Local):
        if not self._enabled or orientation == self._orientation:
            return

        new_transform_matrix = Matrix()
        if transform_space == SceneNode.TransformSpace.Local:
            orientation_matrix = orientation.toMatrix()
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldOrientation() == orientation:
                return
            new_orientation = orientation * (
                self.getWorldOrientation() *
                self._orientation.getInverse()).getInverse()
            orientation_matrix = new_orientation.toMatrix()
        euler_angles = orientation_matrix.getEuler()

        new_transform_matrix.compose(scale=self._scale,
                                     angles=euler_angles,
                                     translate=self._position,
                                     shear=self._shear)
        self._transformation = new_transform_matrix
        self._transformChanged()

    ##  Get the local scaling value.
    def getScale(self):
        return deepcopy(self._scale)

    def getWorldScale(self):
        return deepcopy(self._derived_scale)

    ##  Scale the scene object (and thus its children) by given amount
    #
    #   \param scale \type{Vector} A Vector with three scale values
    #   \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
    def scale(self, scale, transform_space=TransformSpace.Local):
        if not self._enabled:
            return

        scale_matrix = Matrix()
        scale_matrix.setByScaleVector(scale)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(scale_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local scale value.
    #
    #   \param scale \type{Vector} The new scale value of the scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setScale(self, scale, transform_space=TransformSpace.Local):
        if not self._enabled or scale == self._scale:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.scale(scale / self._scale, SceneNode.TransformSpace.Local)
            return
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldScale() == scale:
                return
            self.scale(scale / self._scale, SceneNode.TransformSpace.World)

    ##  Get the local position.
    def getPosition(self):
        return deepcopy(self._position)

    ##  Get the position of this scene node relative to the world.
    def getWorldPosition(self):
        return deepcopy(self._derived_position)

    ##  Translate the scene object (and thus its children) by given amount.
    #
    #   \param translation \type{Vector} The amount to translate by.
    #   \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
    def translate(self, translation, transform_space=TransformSpace.Local):
        if not self._enabled:
            return
        translation_matrix = Matrix()
        translation_matrix.setByTranslation(translation)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(
                self._world_transformation.getInverse())
            self._transformation.multiply(translation_matrix)
            self._transformation.multiply(self._world_transformation)
        self._transformChanged()

    ##  Set the local position value.
    #
    #   \param position The new position value of the SceneNode.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setPosition(self, position, transform_space=TransformSpace.Local):
        if not self._enabled or position == self._position:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.translate(position - self._position,
                           SceneNode.TransformSpace.Parent)
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldPosition() == position:
                return
            self.translate(position - self._position,
                           SceneNode.TransformSpace.World)

    ##  Signal. Emitted whenever the transformation of this object or any child object changes.
    #   \param object The object that caused the change.
    transformationChanged = Signal()

    ##  Rotate this scene node in such a way that it is looking at target.
    #
    #   \param target \type{Vector} The target to look at.
    #   \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
    def lookAt(self, target, up=Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalize()
        up.normalize()
        s = f.cross(up).normalize()
        u = s.cross(f).normalize()

        m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0],
                    [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]])

        self.setOrientation(Quaternion.fromMatrix(m))

    ##  Can be overridden by child nodes if they need to perform special rendering.
    #   If you need to handle rendering in a special way, for example for tool handles,
    #   you can override this method and render the node. Return True to prevent the
    #   view from rendering any attached mesh data.
    #
    #   \param renderer The renderer object to use for rendering.
    #
    #   \return False if the view should render this node, True if we handle our own rendering.
    def render(self, renderer):
        return False

    ##  Get whether this SceneNode is enabled, that is, it can be modified in any way.
    def isEnabled(self):
        if self._parent != None and self._enabled:
            return self._parent.isEnabled()
        else:
            return self._enabled

    ##  Set whether this SceneNode is enabled.
    #   \param enable True if this object should be enabled, False if not.
    #   \sa isEnabled
    def setEnabled(self, enable):
        self._enabled = enable

    ##  Get whether this SceneNode can be selected.
    #
    #   \note This will return false if isEnabled() returns false.
    def isSelectable(self):
        return self._enabled and self._selectable

    ##  Set whether this SceneNode can be selected.
    #
    #   \param select True if this SceneNode should be selectable, False if not.
    def setSelectable(self, select):
        self._selectable = select

    ##  Get the bounding box of this node and its children.
    #
    #   Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB
    #   while the calculation happens.
    def getBoundingBox(self):
        if self._aabb:
            return self._aabb

        if not self._aabb_job:
            self._resetAABB()

        return AxisAlignedBox()

    ##  Set whether or not to calculate the bounding box for this node.
    #
    #   \param calculate True if the bounding box should be calculated, False if not.
    def setCalculateBoundingBox(self, calculate):
        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    ##  private:
    def _transformChanged(self):
        self._updateTransformation()
        self._resetAABB()
        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateTransformation(self):
        scale, shear, euler_angles, translation = self._transformation.decompose(
        )
        self._position = translation
        self._scale = scale
        self._shear = shear
        orientation = Quaternion()
        euler_angle_matrix = Matrix()
        euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y,
                                      euler_angles.z)
        orientation.setByMatrix(euler_angle_matrix)
        self._orientation = orientation
        if self._parent:
            self._world_transformation = self._parent.getWorldTransformation(
            ).multiply(self._transformation, copy=True)
        else:
            self._world_transformation = self._transformation

        world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose(
        )
        self._derived_position = world_translation
        self._derived_scale = world_scale

        world_euler_angle_matrix = Matrix()
        world_euler_angle_matrix.setByEuler(world_euler_angles.x,
                                            world_euler_angles.y,
                                            world_euler_angles.z)
        self._derived_orientation.setByMatrix(world_euler_angle_matrix)

        world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose(
        )

    def _resetAABB(self):
        if not self._calculate_aabb:
            return

        self._aabb = None

        if self._aabb_job:
            self._aabb_job.cancel()

        self._aabb_job = _CalculateAABBJob(self)
        self._aabb_job.start()
예제 #17
0
class SceneNode(SignalEmitter):
    class TransformSpace:
        Local = 1
        Parent = 2
        World = 3

    def __init__(self, parent = None, **kwargs):
        super().__init__() # Call super to make multiple inheritence work.

        self._children = []
        self._mesh_data = None

        self._position = Vector()
        self._scale = Vector(1.0, 1.0, 1.0)
        self._shear = Vector(0.0, 0.0, 0.0)
        self._orientation = Quaternion()

        self._transformation = Matrix() #local transformation
        self._world_transformation = Matrix()

        self._derived_position = Vector()
        self._derived_orientation = Quaternion()
        self._derived_scale = Vector()

        self._inherit_orientation = True
        self._inherit_scale = True

        self._parent = parent
        self._enabled = True
        self._selectable = False
        self._calculate_aabb = True
        self._aabb = None
        self._aabb_job = None
        self._visible = kwargs.get("visible", True)
        self._name = kwargs.get("name", "")
        self._decorators = []
        self._bounding_box_mesh = None
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo):
        copy = SceneNode()
        copy.translate(self.getPosition())
        copy.setOrientation(self.getOrientation())
        copy.setScale(self.getScale())
        copy.setMeshData(deepcopy(self._mesh_data, memo))
        copy.setVisible(deepcopy(self._visible, memo))
        copy._selectable = deepcopy(self._selectable, memo)
        for decorator in self._decorators:
            copy.addDecorator(deepcopy(decorator, memo))

        for child in self._children:
            copy.addChild(deepcopy(child, memo))
        self.calculateBoundingBoxMesh()
        return copy

    def setCenterPosition(self, center):
        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m)
            self._mesh_data.setCenterPosition(center)
        for child in self._children:
            child.setCenterPosition(center)

    ##  \brief Get the parent of this node. If the node has no parent, it is the root node.
    #   \returns SceneNode if it has a parent and None if it's the root node.
    def getParent(self):
        return self._parent

    def getBoundingBoxMesh(self):
        return self._bounding_box_mesh

    def calculateBoundingBoxMesh(self):
        if self._aabb:
            self._bounding_box_mesh = MeshData()
            rtf = self._aabb.maximum
            lbb = self._aabb.minimum

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back
        else:
            self._resetAABB()

    def _onParentChanged(self, node):
        for child in self.getChildren():
            child.parentChanged.emit(self)

    decoratorsChanged = Signal()
    
    def addDecorator(self, decorator):
        decorator.setNode(self)
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    def getDecorators(self):
        return self._decorators

    def getDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator

    def removeDecorators(self):
        self._decorators = []
        self.decoratorsChanged.emit(self)

    def removeDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    def callDecoration(self, function, *args, **kwargs):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e))
                    return None

    def hasDecoration(self, function):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self):
        return self._name

    def setName(self, name):
        self._name = name

    ##  How many nodes is this node removed from the root
    def getDepth(self):
        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    ##  \brief Set the parent of this object
    #   \param scene_node SceneNode that is the parent of this object.
    def setParent(self, scene_node):
        if self._parent:
            self._parent.removeChild(self)
        #self._parent = scene_node

        if scene_node:
            scene_node.addChild(self)

    ##  Emitted whenever the parent changes.
    parentChanged = Signal()

    ##  \brief Get the visibility of this node. The parents visibility overrides the visibility.
    #   TODO: Let renderer actually use the visibility to decide wether to render or not.
    def isVisible(self):
        if self._parent != None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    def setVisible(self, visible):
        self._visible = visible

    ##  \brief Get the (original) mesh data from the scene node/object.
    #   \returns MeshData
    def getMeshData(self):
        return self._mesh_data

    ##  \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.
    #   \returns MeshData
    def getMeshDataTransformed(self):
        #transformed_mesh = deepcopy(self._mesh_data)
        #transformed_mesh.transform(self.getWorldTransformation())
        return self._mesh_data.getTransformed(self.getWorldTransformation())

    ##  \brief Set the mesh of this node/object
    #   \param mesh_data MeshData object
    def setMeshData(self, mesh_data):
        if self._mesh_data:
            self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged)
        self._mesh_data = mesh_data
        if self._mesh_data is not None:
            self._mesh_data.dataChanged.connect(self._onMeshDataChanged)
        self._resetAABB()
        self.meshDataChanged.emit(self)

    ##  Emitted whenever the attached mesh data object changes.
    meshDataChanged = Signal()

    def _onMeshDataChanged(self):
        self.meshDataChanged.emit(self)

    ##  \brief Add a child to this node and set it's parent as this node.
    #   \params scene_node SceneNode to add.
    def addChild(self, scene_node):
        if scene_node not in self._children:
            scene_node.transformationChanged.connect(self.transformationChanged)
            scene_node.childrenChanged.connect(self.childrenChanged)
            scene_node.meshDataChanged.connect(self.meshDataChanged)

            self._children.append(scene_node)
            self._resetAABB()
            self.childrenChanged.emit(self)

            if not scene_node._parent is self:
                scene_node._parent = self
                scene_node._transformChanged()
                scene_node.parentChanged.emit(self)

    ##  \brief remove a single child
    #   \param child Scene node that needs to be removed.
    def removeChild(self, child):
        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self.childrenChanged.emit(self)

    ##  \brief Removes all children and its children's children.
    def removeAllChildren(self):
        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    ##  \brief Get the list of direct children
    #   \returns List of children
    def getChildren(self):
        return self._children

    def hasChildren(self):
        return True if self._children else False

    ##  \brief Get list of all children (including it's children children children etc.)
    #   \returns list ALl children in this 'tree'
    def getAllChildren(self):
        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    ##  \brief Emitted whenever the list of children of this object or any child object changes.
    #   \param object The object that triggered the change.
    childrenChanged = Signal()

    ##  \brief Computes and returns the transformation from world to local space.
    #   \returns 4x4 transformation matrix
    def getWorldTransformation(self):
        if self._world_transformation is None:
            self._updateTransformation()

        return deepcopy(self._world_transformation)

    ##  \brief Returns the local transformation with respect to its parent. (from parent to local)
    #   \retuns transformation 4x4 (homogenous) matrix
    def getLocalTransformation(self):
        if self._transformation is None:
            self._updateTransformation()

        return deepcopy(self._transformation)

    def setTransformation(self, transformation):
        self._transformation = transformation
        self._transformChanged()

    ##  Get the local orientation value.
    def getOrientation(self):
        return deepcopy(self._orientation)

    def getWorldOrientation(self):
        return deepcopy(self._derived_orientation)

    ##  \brief Rotate the scene object (and thus its children) by given amount
    #
    #   \param rotation \type{Quaternion} A quaternion indicating the amount of rotation.
    #   \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
    def rotate(self, rotation, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        orientation_matrix = rotation.toMatrix()
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(orientation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(orientation_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local orientation of this scene node.
    #
    #   \param orientation \type{Quaternion} The new orientation of this scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setOrientation(self, orientation, transform_space = TransformSpace.Local):
        if not self._enabled or orientation == self._orientation:
            return

        new_transform_matrix = Matrix()
        if transform_space == SceneNode.TransformSpace.Local:
            orientation_matrix = orientation.toMatrix()
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldOrientation() == orientation:
                return
            new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse()
            orientation_matrix = new_orientation.toMatrix()
        euler_angles = orientation_matrix.getEuler()

        new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear)
        self._transformation = new_transform_matrix
        self._transformChanged()

    ##  Get the local scaling value.
    def getScale(self):
        return deepcopy(self._scale)

    def getWorldScale(self):
        return deepcopy(self._derived_scale)

    ##  Scale the scene object (and thus its children) by given amount
    #
    #   \param scale \type{Vector} A Vector with three scale values
    #   \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
    def scale(self, scale, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        scale_matrix = Matrix()
        scale_matrix.setByScaleVector(scale)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(scale_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(scale_matrix)
            self._transformation.multiply(self._world_transformation)

        self._transformChanged()

    ##  Set the local scale value.
    #
    #   \param scale \type{Vector} The new scale value of the scene node.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setScale(self, scale, transform_space = TransformSpace.Local):
        if not self._enabled or scale == self._scale:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.scale(scale / self._scale, SceneNode.TransformSpace.Local)
            return
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldScale() == scale:
                return
            self.scale(scale / self._scale, SceneNode.TransformSpace.World)

    ##  Get the local position.
    def getPosition(self):
        return deepcopy(self._position)

    ##  Get the position of this scene node relative to the world.
    def getWorldPosition(self):
        return deepcopy(self._derived_position)

    ##  Translate the scene object (and thus its children) by given amount.
    #
    #   \param translation \type{Vector} The amount to translate by.
    #   \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
    def translate(self, translation, transform_space = TransformSpace.Local):
        if not self._enabled:
            return
        translation_matrix = Matrix()
        translation_matrix.setByTranslation(translation)
        if transform_space == SceneNode.TransformSpace.Local:
            self._transformation.multiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._transformation.preMultiply(translation_matrix)
        elif transform_space == SceneNode.TransformSpace.World:
            self._transformation.multiply(self._world_transformation.getInverse())
            self._transformation.multiply(translation_matrix)
            self._transformation.multiply(self._world_transformation)
        self._transformChanged()

    ##  Set the local position value.
    #
    #   \param position The new position value of the SceneNode.
    #   \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace.
    def setPosition(self, position, transform_space = TransformSpace.Local):
        if not self._enabled or position == self._position:
            return
        if transform_space == SceneNode.TransformSpace.Local:
            self.translate(position - self._position, SceneNode.TransformSpace.Parent)
        if transform_space == SceneNode.TransformSpace.World:
            if self.getWorldPosition() == position:
                return
            self.translate(position - self._position, SceneNode.TransformSpace.World)

    ##  Signal. Emitted whenever the transformation of this object or any child object changes.
    #   \param object The object that caused the change.
    transformationChanged = Signal()

    ##  Rotate this scene node in such a way that it is looking at target.
    #
    #   \param target \type{Vector} The target to look at.
    #   \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
    def lookAt(self, target, up = Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalize()
        up.normalize()
        s = f.cross(up).normalize()
        u = s.cross(f).normalize()

        m = Matrix([
            [ s.x,  u.x,  -f.x, 0.0],
            [ s.y,  u.y,  -f.y, 0.0],
            [ s.z,  u.z,  -f.z, 0.0],
            [ 0.0,  0.0,  0.0,  1.0]
        ])


        self.setOrientation(Quaternion.fromMatrix(m))

    ##  Can be overridden by child nodes if they need to perform special rendering.
    #   If you need to handle rendering in a special way, for example for tool handles,
    #   you can override this method and render the node. Return True to prevent the
    #   view from rendering any attached mesh data.
    #
    #   \param renderer The renderer object to use for rendering.
    #
    #   \return False if the view should render this node, True if we handle our own rendering.
    def render(self, renderer):
        return False

    ##  Get whether this SceneNode is enabled, that is, it can be modified in any way.
    def isEnabled(self):
        if self._parent != None and self._enabled:
            return self._parent.isEnabled()
        else:
            return self._enabled

    ##  Set whether this SceneNode is enabled.
    #   \param enable True if this object should be enabled, False if not.
    #   \sa isEnabled
    def setEnabled(self, enable):
        self._enabled = enable

    ##  Get whether this SceneNode can be selected.
    #
    #   \note This will return false if isEnabled() returns false.
    def isSelectable(self):
        return self._enabled and self._selectable

    ##  Set whether this SceneNode can be selected.
    #
    #   \param select True if this SceneNode should be selectable, False if not.
    def setSelectable(self, select):
        self._selectable = select

    ##  Get the bounding box of this node and its children.
    #
    #   Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB
    #   while the calculation happens.
    def getBoundingBox(self):
        if self._aabb:
            return self._aabb

        if not self._aabb_job:
            self._resetAABB()

        return AxisAlignedBox()

    ##  Set whether or not to calculate the bounding box for this node.
    #
    #   \param calculate True if the bounding box should be calculated, False if not.
    def setCalculateBoundingBox(self, calculate):
        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    ##  private:
    def _transformChanged(self):
        self._updateTransformation()
        self._resetAABB()
        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateTransformation(self):
        scale, shear, euler_angles, translation = self._transformation.decompose()
        self._position = translation
        self._scale = scale
        self._shear = shear
        orientation = Quaternion()
        euler_angle_matrix = Matrix()
        euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z)
        orientation.setByMatrix(euler_angle_matrix)
        self._orientation = orientation
        if self._parent:
            self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True)
        else:
            self._world_transformation = self._transformation

        world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose()
        self._derived_position = world_translation
        self._derived_scale = world_scale

        world_euler_angle_matrix = Matrix()
        world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z)
        self._derived_orientation.setByMatrix(world_euler_angle_matrix)

        world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose()

    def _resetAABB(self):
        if not self._calculate_aabb:
            return

        self._aabb = None

        if self._aabb_job:
            self._aabb_job.cancel()

        self._aabb_job = _CalculateAABBJob(self)
        self._aabb_job.start()
예제 #18
0
class Camera(SceneNode.SceneNode):
    def __init__(self,
                 name: str = "",
                 parent: SceneNode.SceneNode = None) -> None:
        super().__init__(parent)
        self._name = name  # type: str
        self._projection_matrix = Matrix()  # type: Matrix
        self._projection_matrix.setOrtho(-5, 5, 5, -5, -100, 100)
        self._perspective = True  # type: bool
        self._viewport_width = 0  # type: int
        self._viewport_height = 0  # type: int
        self._window_width = 0  # type: int
        self._window_height = 0  # type: int
        self._auto_adjust_view_port_size = True  # type: bool
        self.setCalculateBoundingBox(False)
        self._cached_view_projection_matrix = None  # type: Optional[Matrix]

    def __deepcopy__(self, memo: Dict[int, object]) -> "Camera":
        copy = cast(Camera, super().__deepcopy__(memo))
        copy._projection_matrix = self._projection_matrix
        copy._window_height = self._window_height
        copy._window_width = self._window_width
        copy._viewport_height = self._viewport_height
        copy._viewport_width = self._viewport_width
        return copy

    def setMeshData(self, mesh_data: Optional["MeshData"]) -> None:
        assert mesh_data is None, "Camera's can't have mesh data"

    def getAutoAdjustViewPort(self) -> bool:
        return self._auto_adjust_view_port_size

    def setAutoAdjustViewPort(self, auto_adjust: bool) -> None:
        self._auto_adjust_view_port_size = auto_adjust

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self) -> Matrix:
        return self._projection_matrix

    def getViewportWidth(self) -> int:
        return self._viewport_width

    def setViewportWidth(self, width: int) -> None:
        self._viewport_width = width

    def setViewportHeight(self, height: int) -> None:
        self._viewport_height = height

    def setViewportSize(self, width: int, height: int) -> None:
        self._viewport_width = width
        self._viewport_height = height

    def getViewProjectionMatrix(self):
        if self._cached_view_projection_matrix is None:
            inverted_transformation = self.getWorldTransformation()
            inverted_transformation.invert()
            self._cached_view_projection_matrix = self._projection_matrix.multiply(
                inverted_transformation, copy=True)
        return self._cached_view_projection_matrix

    def _updateWorldTransformation(self):
        self._cached_view_projection_matrix = None
        super()._updateWorldTransformation()

    def getViewportHeight(self) -> int:
        return self._viewport_height

    def setWindowSize(self, width: int, height: int) -> None:
        self._window_width = width
        self._window_height = height

    def getWindowSize(self) -> Tuple[int, int]:
        return self._window_width, self._window_height

    ##  Set the projection matrix of this camera.
    #   \param matrix The projection matrix to use for this camera.
    def setProjectionMatrix(self, matrix: Matrix) -> None:
        self._projection_matrix = matrix
        self._cached_view_projection_matrix = None

    def isPerspective(self) -> bool:
        return self._perspective

    def setPerspective(self, perspective: bool) -> None:
        self._perspective = perspective

    ##  Get a ray from the camera into the world.
    #
    #   This will create a ray from the camera's origin, passing through (x, y)
    #   on the near plane and continuing based on the projection matrix.
    #
    #   \param x The X coordinate on the near plane this ray should pass through.
    #   \param y The Y coordinate on the near plane this ray should pass through.
    #
    #   \return A Ray object representing a ray from the camera origin through X, Y.
    #
    #   \note The near-plane coordinates should be in normalized form, that is within (-1, 1).
    def getRay(self, x: float, y: float) -> Ray:
        window_x = ((x + 1) / 2) * self._window_width
        window_y = ((y + 1) / 2) * self._window_height
        view_x = (window_x / self._viewport_width) * 2 - 1
        view_y = (window_y / self._viewport_height) * 2 - 1

        inverted_projection = numpy.linalg.inv(
            self._projection_matrix.getData().copy())
        transformation = self.getWorldTransformation().getData()

        near = numpy.array([view_x, -view_y, -1.0, 1.0], dtype=numpy.float32)
        near = numpy.dot(inverted_projection, near)
        near = numpy.dot(transformation, near)
        near = near[0:3] / near[3]

        far = numpy.array([view_x, -view_y, 1.0, 1.0], dtype=numpy.float32)
        far = numpy.dot(inverted_projection, far)
        far = numpy.dot(transformation, far)
        far = far[0:3] / far[3]

        direction = far - near
        direction /= numpy.linalg.norm(direction)

        return Ray(self.getWorldPosition(),
                   Vector(-direction[0], -direction[1], -direction[2]))

    ##  Project a 3D position onto the 2D view plane.
    def project(self, position: Vector) -> Tuple[float, float]:
        projection = self._projection_matrix
        view = self.getWorldTransformation()
        view.invert()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)
        return position.x / position.z / 2.0, position.y / position.z / 2.0
예제 #19
0
    def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
        self._empty_project = False
        result = []
        # The base object of 3mf is a zipped archive.
        try:
            archive = zipfile.ZipFile(file_name, "r")
            self._base_name = os.path.basename(file_name)
            parser = Savitar.ThreeMFParser()
            scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
            self._unit = scene_3mf.getUnit()

            for key, value in scene_3mf.getMetadata().items():
                CuraApplication.getInstance().getController().getScene(
                ).setMetaDataEntry(key, value)

            for node in scene_3mf.getSceneNodes():
                um_node = self._convertSavitarNodeToUMNode(node, file_name)
                if um_node is None:
                    continue

                # compensate for original center position, if object(s) is/are not around its zero position
                transform_matrix = Matrix()
                mesh_data = um_node.getMeshData()
                if mesh_data is not None:
                    extents = mesh_data.getExtents()
                    if extents is not None:
                        center_vector = Vector(extents.center.x,
                                               extents.center.y,
                                               extents.center.z)
                        transform_matrix.setByTranslation(center_vector)
                transform_matrix.multiply(um_node.getLocalTransformation())
                um_node.setTransformation(transform_matrix)

                global_container_stack = CuraApplication.getInstance(
                ).getGlobalContainerStack()

                # Create a transformation Matrix to convert from 3mf worldspace into ours.
                # First step: flip the y and z axis.
                transformation_matrix = Matrix()
                transformation_matrix._data[1, 1] = 0
                transformation_matrix._data[1, 2] = 1
                transformation_matrix._data[2, 1] = -1
                transformation_matrix._data[2, 2] = 0

                # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
                # build volume.
                if global_container_stack:
                    translation_vector = Vector(
                        x=-global_container_stack.getProperty(
                            "machine_width", "value") / 2,
                        y=-global_container_stack.getProperty(
                            "machine_depth", "value") / 2,
                        z=0)
                    translation_matrix = Matrix()
                    translation_matrix.setByTranslation(translation_vector)
                    transformation_matrix.multiply(translation_matrix)

                # Third step: 3MF also defines a unit, whereas Cura always assumes mm.
                scale_matrix = Matrix()
                scale_matrix.setByScaleVector(
                    self._getScaleFromUnit(self._unit))
                transformation_matrix.multiply(scale_matrix)

                # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
                um_node.setTransformation(
                    um_node.getLocalTransformation().preMultiply(
                        transformation_matrix))

                # Check if the model is positioned below the build plate and honor that when loading project files.
                node_meshdata = um_node.getMeshData()
                if node_meshdata is not None:
                    aabb = node_meshdata.getExtents(
                        um_node.getWorldTransformation())
                    if aabb is not None:
                        minimum_z_value = aabb.minimum.y  # y is z in transformation coordinates
                        if minimum_z_value < 0:
                            um_node.addDecorator(ZOffsetDecorator())
                            um_node.callDecoration("setZOffset",
                                                   minimum_z_value)

                result.append(um_node)

            if len(result) == 0:
                self._empty_project = True

        except Exception:
            Logger.logException("e", "An exception occurred in 3mf reader.")
            return []

        return result
예제 #20
0
class GenerateBasementJob(Job):
    processingProgress = Signal()
    timeMaterialEstimates = Signal()

    def __init__(self):
        super().__init__()
        self._layer_data_builder = LayerDataBuilder()
        self._abort_requested = False
        self._build_plate_number = None

        self._gcode_list = []
        self._material_amounts = [0.0, 0.0]
        self._times = {
            "inset_0": 0,
            "inset_x": 0,
            "skin": 0,
            "infill": 0,
            "support_infill": 0,
            "support_interface": 0,
            "support": 0,
            "skirt": 0,
            "travel": 0,
            "retract": 0,
            "none": 0
        }
        self._position = Position(0, 0, 0, 0, 0, 1, 0, [0])
        self._gcode_position = Position(999, 999, 999, 0, 0, 0, 0, [0])
        self._first_move = True
        self._rot_nwp = Matrix()
        self._rot_nws = Matrix()
        self._pi_faction = 0

        self._global_stack = SteSlicerApplication.getInstance(
        ).getGlobalContainerStack()
        stack = self._global_stack.getTop()
        self._travel_speed = self._global_stack.getProperty(
            "speed_travel", "value")
        self._raft_base_thickness = self._global_stack.getProperty(
            "raft_base_thickness", "value")
        self._raft_base_line_width = self._global_stack.getProperty(
            "raft_base_line_width", "value")
        self._raft_base_line_spacing = self._global_stack.getProperty(
            "raft_base_line_spacing", "value")
        self._raft_speed = self._global_stack.getProperty(
            "raft_speed", "value")
        self._raft_margin = self._global_stack.getProperty(
            "raft_margin", "value")
        self._extruder_number = 0
        self._extruder_offsets = {}
        extruder = self._global_stack.extruders.get(
            "%s" % self._extruder_number,
            None)  # type: Optional[ExtruderStack]
        self._filament_diameter = extruder.getProperty("material_diameter",
                                                       "value")

        self._cylindrical_raft_enabled = stack.getProperty(
            "cylindrical_raft_enabled", "value")
        if self._cylindrical_raft_enabled is None:
            self._cylindrical_raft_enabled = self._global_stack.getProperty(
                "cylindrical_raft_enabled", "value")
        self._cylindrical_mode_base_diameter = self._global_stack.getProperty(
            "cylindrical_raft_diameter", "value")
        self._non_printing_base_diameter = self._global_stack.getProperty(
            "non_printing_base_diameter", "value")
        self._cylindrical_raft_base_height = self._global_stack.getProperty(
            "cylindrical_raft_base_height", "value")

        self._enable_retraction = extruder.getProperty("retraction_enable",
                                                       "value")
        self._retraction_amount = extruder.getProperty("retraction_amount",
                                                       "value")
        self._retraction_min_travel = extruder.getProperty(
            "retraction_min_travel", "value")
        self._retraction_hop_enabled = extruder.getProperty(
            "retraction_hop_enabled", "value")
        self._retraction_hop = extruder.getProperty("retraction_hop", "value")
        self._retraction_speed = self._global_stack.getProperty(
            "retraction_retract_speed", "value")
        self._prime_speed = self._global_stack.getProperty(
            "retraction_prime_speed", "value")

        self._machine_a_axis_coefficient = self._global_stack.getProperty(
            "machine_a_axis_multiplier",
            "value") / self._global_stack.getProperty("machine_a_axis_divider",
                                                      "value")
        self._machine_c_axis_coefficient = self._global_stack.getProperty(
            "machine_c_axis_multiplier",
            "value") / self._global_stack.getProperty("machine_c_axis_divider",
                                                      "value")

    def abort(self):
        self._abort_requested = True

    def isCancelled(self) -> bool:
        return self._abort_requested

    def setBuildPlate(self, new_value):
        self._build_plate_number = new_value

    def getBuildPlate(self):
        return self._build_plate_number

    def getGCodeList(self):
        return self._gcode_list

    def getLayersData(self):
        return self._layer_data_builder.getLayers().values()

    def getMaterialAmounts(self):
        return self._material_amounts

    def getTimes(self):
        return self._times

    def run(self):
        self._gcode_list = []
        self._material_amounts = [0.0, 0.0]
        self._times = {
            "inset_0": 0,
            "inset_x": 0,
            "skin": 0,
            "infill": 0,
            "support_infill": 0,
            "support_interface": 0,
            "support": 0,
            "skirt": 0,
            "travel": 0,
            "retract": 0,
            "none": 0
        }

        Logger.log("d", "Generating basement...")

        if not self._cylindrical_raft_enabled:
            self._gcode_list.append("G0 A0 F600\nG92 E0 C0\n")
            return

        self._position = Position(0, 0, 0, 0, 0, 1, 0, [0])
        self._gcode_position = Position(999, 999, 999, 0, 0, 0, 0, [0])
        self._first_move = True
        current_path = []  # type: List[List[float]]

        layer_count = int((self._cylindrical_mode_base_diameter -
                           self._non_printing_base_diameter) /
                          (2 * self._raft_base_thickness))

        for layer_number in range(0, layer_count):
            if self._abort_requested:
                Logger.log("d", "Parsing basement file cancelled")
                return
            self.processingProgress.emit(layer_number / layer_count)
            self._gcode_list.append(";LAYER:%s\n" % layer_number)

            self._gcode_list[-1] = self.processPolyline(
                layer_number, current_path, self._gcode_list[-1], layer_count)

            self._createPolygon(
                layer_number, current_path,
                self._extruder_offsets.get(self._extruder_number, [0, 0]))
            current_path.clear()

            if self._abort_requested:
                return

            Job.yieldThread()

        self._gcode_list.append(
            "G91\nG0 Z50\nG90\nG54\nG0 Z100 A0 F600\nG92 E0 C0\nG1 F200 E-2\nG92 E0 ;zero the extruded length again\nG55\nG1 F200 E2\nG92 E0 ;zero the extruded length again\n"
        )

    def processPolyline(self, layer_number: int, path: List[List[Union[float,
                                                                       int]]],
                        gcode_line: str, layer_count: int) -> str:
        radius = self._non_printing_base_diameter / 2 + (
            self._raft_base_thickness * (layer_number + 1))
        height = self._cylindrical_raft_base_height - layer_number * self._raft_base_line_width / 3
        if height < self._raft_base_line_width * 2:
            height = self._raft_base_line_width * 2
        points = self._generateHelix(radius, height, layer_number, False)

        new_position, new_gcode_position = points[0]

        is_retraction = self._enable_retraction and self._positionLength(
            self._position, new_position
        ) > self._retraction_min_travel and not self._first_move
        if is_retraction:
            # we have retraction move
            new_extruder_position = self._position.e[
                self._extruder_number] - self._retraction_amount
            gcode_line += "G1 E%.5f F%.0f\n" % (new_extruder_position,
                                                (self._retraction_speed * 60))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[
                self._extruder_number] = new_extruder_position
            self._addToPath(path, [
                self._position.x, self._position.y, self._position.z,
                self._position.a, self._position.b, self._position.c,
                self._retraction_speed, self._position.e,
                LayerPolygon.MoveRetractionType
            ])
            # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
            #             self._position.c, self._retraction_speed, self._position.e, LayerPolygon.MoveRetractionType])

            if self._retraction_hop_enabled:
                # add hop movement
                gx, gy, gz, ga, gb, gc, gf, ge = self._gcode_position
                x, y, z, a, b, c, f, e = self._position
                gcode_position = Position(gx, gy, gz + self._retraction_hop,
                                          ga, gb, gc, self._travel_speed, ge)
                self._position = Position(x + a * self._retraction_hop,
                                          y + b * self._retraction_hop,
                                          z + c * self._retraction_hop, a, b,
                                          c, self._travel_speed, e)
                gcode_command = self._generateGCodeCommand(
                    0, gcode_position, self._travel_speed)
                if gcode_command is not None:
                    gcode_line += gcode_command
                self._gcode_position = gcode_position
                self._addToPath(path, [
                    self._position.x, self._position.y, self._position.z,
                    self._position.a, self._position.b, self._position.c,
                    self._prime_speed, self._position.e,
                    LayerPolygon.MoveCombingType
                ])
                # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                #             self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveCombingType])
                gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
                x, y, z, a, b, c, f, e = new_position
                gcode_position = Position(gx, gy, gz + self._retraction_hop,
                                          ga, gb, gc, self._travel_speed, ge)
                position = Position(x + a * self._retraction_hop,
                                    y + b * self._retraction_hop,
                                    z + c * self._retraction_hop, a, b, c,
                                    self._travel_speed, e)
                gcode_command = self._generateGCodeCommand(
                    0, gcode_position, self._travel_speed)
                if gcode_command is not None:
                    gcode_line += gcode_command
                self._addToPath(path, [
                    position.x, position.y, position.z, position.a, position.b,
                    position.c, position.f, position.e,
                    LayerPolygon.MoveCombingType
                ])
                # path.append([position.x, position.y, position.z, position.a, position.b,
                #             position.c, position.f, position.e, LayerPolygon.MoveCombingType])

        feedrate = self._travel_speed
        x, y, z, a, b, c, f, e = new_position
        self._position = Position(x, y, z, a, b, c, feedrate, self._position.e)
        gcode_command = self._generateGCodeCommand(0, new_gcode_position,
                                                   feedrate)
        if gcode_command is not None:
            gcode_line += gcode_command
        gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
        self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate, ge)
        self._addToPath(
            path,
            [x, y, z, a, b, c, feedrate, e, LayerPolygon.MoveCombingType])
        self._first_move = False
        if is_retraction:
            # we have retraction move
            new_extruder_position = self._position.e[
                self._extruder_number] + self._retraction_amount
            gcode_line += "G1 E%.5f F%.0f\n" % (new_extruder_position,
                                                (self._prime_speed * 60))
            self._position.e[self._extruder_number] = new_extruder_position
            self._gcode_position.e[
                self._extruder_number] = new_extruder_position
            self._addToPath(path, [
                self._position.x, self._position.y, self._position.z,
                self._position.a, self._position.b, self._position.c,
                self._prime_speed, self._position.e,
                LayerPolygon.MoveRetractionType
            ])
            # path.append([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
            #             self._position.c, self._prime_speed, self._position.e, LayerPolygon.MoveRetractionType])

        gcode_line += ";TYPE:SKIRT\n"
        points.pop(0)
        for point in points:
            new_position, new_gcode_position = point
            feedrate = self._raft_speed
            x, y, z, a, b, c, f, e = new_position
            self._position = Position(x, y, z, a, b, c, feedrate, e)
            gcode_command = self._generateGCodeCommand(1, new_gcode_position,
                                                       feedrate)
            if gcode_command is not None:
                gcode_line += gcode_command
            gx, gy, gz, ga, gb, gc, gf, ge = new_gcode_position
            self._gcode_position = Position(gx, gy, gz, ga, gb, gc, feedrate,
                                            ge)
            self._addToPath(
                path, [x, y, z, a, b, c, feedrate, e, LayerPolygon.SkirtType])
        return gcode_line

    def _generateHelix(self,
                       radius: float,
                       height: float,
                       layer_number: int,
                       reverse_twist: bool,
                       chordal_err: float = 0.025):
        pitch = self._raft_base_line_width
        max_t = numpy.pi * 2 + height / pitch
        result = []
        position = self._position
        gcode_position = self._gcode_position
        for t in numpy.arange(0, max_t, chordal_err):
            x = radius * cos(t)
            y = radius * (sin(t) if not reverse_twist else -sin(t))
            z = -self._raft_base_line_width / 2 if max_t - t <= (
                numpy.pi + chordal_err) * 2 else -(height - pitch * t)
            length = numpy.sqrt(x**2 + y**2)
            i = x / length if length != 0 else 0
            j = y / length if length != 0 else 0
            k = 0
            new_position = Position(x, y, z, i, j, k, 0, [0])
            new_gcode_position = self._transformCoordinates(
                x, y, z, i, j, k, gcode_position)
            new_position.e[self._extruder_number] = position.e[
                self._extruder_number] + self._calculateExtrusion(
                    [x, y, z],
                    position) if t > 0.0 else position.e[self._extruder_number]
            new_gcode_position.e[self._extruder_number] = new_position.e[
                self._extruder_number]
            position = new_position
            gcode_position = new_gcode_position
            result.append((new_position, new_gcode_position))
        #if layer_number == 0:
        for t in numpy.arange(max_t, 2 * max_t - numpy.pi * 2, chordal_err):
            x = -radius * cos(t - numpy.pi)
            y = radius * (sin(t) if reverse_twist else -sin(t - numpy.pi))
            z = -pitch * (t - max_t)
            length = numpy.sqrt(x**2 + y**2)
            i = x / length if length != 0 else 0
            j = y / length if length != 0 else 0
            k = 0
            new_position = Position(x, y, z, i, j, k, 0, [0])
            new_gcode_position = self._transformCoordinates(
                x, y, z, i, j, k, gcode_position)
            new_position.e[self._extruder_number] = position.e[
                self._extruder_number] + self._calculateExtrusion(
                    [x, y, z],
                    position) if t > 0.0 else position.e[self._extruder_number]
            new_gcode_position.e[self._extruder_number] = new_position.e[
                self._extruder_number]
            position = new_position
            gcode_position = new_gcode_position
            result.append((new_position, new_gcode_position))
        return result

    def _transformCoordinates(
            self, x: float, y: float, z: float, i: float, j: float, k: float,
            position: Position) -> (float, float, float, float, float, float):
        a = position.a
        c = position.c
        # Get coordinate angles
        if abs(self._position.c - k) > 0.00001:
            a = numpy.arccos(k)
            self._rot_nwp = Matrix()
            self._setByRotationAxis(self._rot_nwp, -a, Vector.Unit_X)
            # self._rot_nwp.setByRotationAxis(-a, Vector.Unit_X)
            a = numpy.degrees(a)
        if abs(self._position.a - i) > 0.00001 or abs(self._position.b -
                                                      j) > 0.00001:
            c = numpy.arctan2(j, i) if x != 0 and y != 0 else 0
            angle = numpy.degrees(c + self._pi_faction * 2 * numpy.pi)
            if abs(angle - position.c) > 180:
                self._pi_faction += 1 if (angle - position.c) < 0 else -1
            c += self._pi_faction * 2 * numpy.pi
            c -= numpy.pi / 2
            self._rot_nws = Matrix()
            self._setByRotationAxis(self._rot_nws, c, Vector.Unit_Z)
            # self._rot_nws.setByRotationAxis(c, Vector.Unit_Z)
            c = numpy.degrees(c)

        tr = self._rot_nws.multiply(self._rot_nwp, True)
        tr.invert()
        pt = Vector(data=numpy.array([x, y, z, 1]))
        ret = tr.multiply(pt, True).getData()

        return Position(ret[0], ret[1], ret[2], a, 0, c, 0, [0])

    def _setByRotationAxis(self,
                           matrix,
                           angle: float,
                           direction: Vector,
                           point: Optional[List[float]] = None) -> None:
        sina = numpy.sin(angle)
        cosa = numpy.cos(angle)
        direction_data = matrix._unitVector(direction.getData())
        # rotation matrix around unit vector
        R = numpy.diag([cosa, cosa, cosa])
        R += numpy.outer(direction_data, direction_data) * (1.0 - cosa)
        direction_data *= sina
        R += numpy.array([[0.0, -direction_data[2], direction_data[1]],
                          [direction_data[2], 0.0, -direction_data[0]],
                          [-direction_data[1], direction_data[0], 0.0]],
                         dtype=numpy.float64)
        M = numpy.identity(4)
        M[:3, :3] = R
        if point is not None:
            # rotation not around origin
            point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
            M[:3, 3] = point - numpy.dot(R, point)
        matrix._data = M

    def _calculateExtrusion(self, current_point: List[float],
                            previous_point: Position) -> float:

        Af = (self._filament_diameter / 2)**2 * 3.14
        Al = self._raft_base_line_width * self._raft_base_thickness
        de = numpy.sqrt((current_point[0] - previous_point[0])**2 +
                        (current_point[1] - previous_point[1])**2 +
                        (current_point[2] - previous_point[2])**2)
        dVe = Al * de
        self._material_amounts[self._extruder_number] += float(dVe)
        return dVe / Af

    def _createPolygon(self, layer_number: int, path: List[List[Union[float,
                                                                      int]]],
                       extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[8] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(layer_number)
            self._layer_data_builder.setLayerHeight(
                layer_number, self._raft_base_thickness * (layer_number + 1))
            self._layer_data_builder.setLayerThickness(
                layer_number, self._raft_base_thickness)
            this_layer = self._layer_data_builder.getLayer(layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = self._raft_base_line_width
        line_thicknesses[:, 0] = self._raft_base_thickness
        points = numpy.empty((count, 6), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:

            points[i, :] = [
                point[0] + extruder_offsets[0], point[2],
                -point[1] - extruder_offsets[1], -point[4], point[5], -point[3]
            ]
            extrusion_values[i] = point[7]
            if i > 0:
                line_feedrates[i - 1] = point[6]
                line_types[i - 1] = point[8]
                if point[8] in [
                        LayerPolygon.MoveCombingType,
                        LayerPolygon.MoveRetractionType
                ]:
                    line_widths[i - 1] = 0.1
                    # Travels are set as zero thickness lines
                    line_thicknesses[i - 1] = 0.0
                else:
                    line_widths[i - 1] = self._raft_base_line_width
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points,
                                 line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _addToPath(self, path: List[List[Union[float, int]]],
                   addition: List[Union[float, int]]):
        layer_type = addition[8]
        layer_type_to_times_type = {
            LayerPolygon.NoneType: "none",
            LayerPolygon.Inset0Type: "inset_0",
            LayerPolygon.InsetXType: "inset_x",
            LayerPolygon.SkinType: "skin",
            LayerPolygon.SupportType: "support",
            LayerPolygon.SkirtType: "skirt",
            LayerPolygon.InfillType: "infill",
            LayerPolygon.SupportInfillType: "support_infill",
            LayerPolygon.MoveCombingType: "travel",
            LayerPolygon.MoveRetractionType: "retract",
            LayerPolygon.SupportInterfaceType: "support_interface"
        }
        if len(path) > 0:
            last_point = path[-1]
        else:
            last_point = addition
        length = numpy.sqrt((last_point[0] - addition[0])**2 +
                            (last_point[1] - addition[1])**2 +
                            (last_point[2] - addition[2])**2)
        feedrate = addition[6]
        if feedrate == 0:
            feedrate = self._travel_speed
        self._times[layer_type_to_times_type[layer_type]] += (length /
                                                              feedrate) * 2
        path.append(addition)

    @staticmethod
    def _positionLength(start: Position, end: Position) -> float:
        return numpy.sqrt((start.x - end.x)**2 + (start.y - end.y)**2 +
                          (start.z - end.z)**2)

    def _generateGCodeCommand(self, g: int, gcode_position: Position,
                              feedrate: float) -> Optional[str]:
        gcode_command = "G%s" % g
        if numpy.abs(gcode_position.x - self._gcode_position.x) > 0.0001:
            gcode_command += " X%.2f" % gcode_position.x
        if numpy.abs(gcode_position.y - self._gcode_position.y) > 0.0001:
            gcode_command += " Y%.2f" % gcode_position.y
        if numpy.abs(gcode_position.z - self._gcode_position.z) > 0.0001:
            gcode_command += " Z%.2f" % gcode_position.z
        if numpy.abs(gcode_position.a - self._gcode_position.a) > 0.0001:
            gcode_command += " A%.2f" % (gcode_position.a *
                                         self._machine_a_axis_coefficient)
        if numpy.abs(gcode_position.b - self._gcode_position.b) > 0.0001:
            gcode_command += " B%.2f" % gcode_position.b
        if numpy.abs(gcode_position.c - self._gcode_position.c) > 0.0001:
            gcode_command += " C%.3f" % (gcode_position.c *
                                         self._machine_c_axis_coefficient)
        if numpy.abs(feedrate - self._gcode_position.f) > 0.0001:
            gcode_command += " F%.0f" % (feedrate * 60)
        if numpy.abs(gcode_position.e[self._extruder_number] -
                     self._gcode_position.e[self._extruder_number]
                     ) > 0.0001 and g > 0:
            gcode_command += " E%.5f" % gcode_position.e[self._extruder_number]
        gcode_command += "\n"
        if gcode_command != "G%s\n" % g:
            return gcode_command
        else:
            return None