예제 #1
0
class TestMatrix(unittest.TestCase):
    def setUp(self):
        self._matrix = Matrix()
        # Called before the first testfunction is executed
        pass

    def tearDown(self):
        # Called after the last testfunction was executed
        pass

    def test_setByQuaternion(self):
        pass
    
    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]]))
    
    def test_preMultiply(self):
        temp_matrix = Matrix()
        temp_matrix.setByTranslation(Vector(10,10,10))
        temp_matrix2 = Matrix()
        temp_matrix2.setByScaleFactor(0.5)
        temp_matrix.preMultiply(temp_matrix2)
        numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,5],[0,0.5,0,5],[0,0,0.5,5],[0,0,0,1]]))

    def test_setByScaleFactor(self):
        self._matrix.setByScaleFactor(0.5)
        numpy.testing.assert_array_almost_equal(self._matrix.getData(), numpy.array([[0.5,0,0,0],[0,0.5,0,0],[0,0,0.5,0],[0,0,0,1]]))

    def test_setByRotation(self):
        pass

    def test_setByTranslation(self):
        self._matrix.setByTranslation(Vector(0,1,0))
        numpy.testing.assert_array_almost_equal(self._matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,1],[0,0,1,0],[0,0,0,1]]))

    def test_setToIdentity(self):
        pass

    def test_getData(self):
        pass

    def test_transposed(self):
        temp_matrix = Matrix()  
        temp_matrix.setByTranslation(Vector(10,10,10))
        temp_matrix = temp_matrix.getTransposed()
        numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[10,10,10,1]]))

    def test_dot(self):
        pass
예제 #2
0
    def _rotateCamera(self, x, y):
        camera = self._scene.getActiveCamera()
        if not camera or not camera.isEnabled():
            return

        self._scene.acquireLock()

        dx = math.radians(x * 180.0)
        dy = math.radians(y * 180.0)

        diff = camera.getPosition() - self._origin
        my = Matrix()
        my.setByRotationAxis(dx, Vector.Unit_Y)

        mx = Matrix(my.getData())
        mx.rotateByAxis(dy, Vector.Unit_Y.cross(diff).normalized())

        n = diff.multiply(mx)

        try:
            angle = math.acos(Vector.Unit_Y.dot(n.normalized()))
        except ValueError:
            return

        if angle < 0.1 or angle > math.pi - 0.1:
            n = diff.multiply(my)

        n += self._origin

        camera.setPosition(n)
        camera.lookAt(self._origin)

        self._scene.releaseLock()
예제 #3
0
 def test_preMultiply(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10,10,10))
     temp_matrix2 = Matrix()
     temp_matrix2.setByScaleFactor(0.5)
     temp_matrix.preMultiply(temp_matrix2)
     numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,5],[0,0.5,0,5],[0,0,0.5,5],[0,0,0,1]]))
예제 #4
0
def test_getterAndSetters():
    # Pretty much all of them are super simple, but it doesn't hurt to check them.
    camera = Camera()

    camera.setAutoAdjustViewPort(False)
    assert camera.getAutoAdjustViewPort() == False

    camera.setViewportWidth(12)
    assert camera.getViewportWidth() == 12

    camera.setViewportHeight(12)
    assert camera.getViewportHeight() == 12

    camera.setViewportSize(22, 22)
    assert camera.getViewportHeight() == 22
    assert camera.getViewportWidth() == 22

    camera.setWindowSize(9001, 9002)
    assert camera.getWindowSize() == (9001, 9002)

    camera.setPerspective(False)
    assert camera.isPerspective() == False

    matrix = Matrix()
    matrix.setPerspective(10, 20, 30, 40)
    camera.setProjectionMatrix(matrix)

    assert numpy.array_equal(camera.getProjectionMatrix().getData(), matrix.getData())
예제 #5
0
 def test_transposed(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10, 10, 10))
     temp_matrix = temp_matrix.getTransposed()
     numpy.testing.assert_array_almost_equal(
         temp_matrix.getData(),
         numpy.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0],
                      [10, 10, 10, 1]]))
예제 #6
0
def transformVertices(vertices: numpy.ndarray,
                      transformation: Matrix) -> numpy.ndarray:
    data = numpy.pad(vertices, ((0, 0), (0, 1)),
                     "constant",
                     constant_values=(0.0, 0.0))
    data = data.dot(transformation.getTransposed().getData())
    data += transformation.getData()[:, 3]
    data = data[:, 0:3]
    return data
예제 #7
0
    def test_setByScaleFactor(self):
        matrix = Matrix()
        matrix.setByScaleFactor(0.5)
        numpy.testing.assert_array_almost_equal(
            matrix.getData(),
            numpy.array([[0.5, 0, 0, 0], [0, 0.5, 0, 0], [0, 0, 0.5, 0],
                         [0, 0, 0, 1]]))

        assert matrix.getScale() == Vector(0.5, 0.5, 0.5)
예제 #8
0
    def pywimBoundaryCondition(self, step: pywim.chop.model.Step,
                               mesh_rotation: Matrix):

        force = pywim.chop.model.Force(name=self.getName())

        load_vec = self.activeArrow.direction.normalized(
        ) * self.force.magnitude
        rotated_load_vec = numpy.dot(mesh_rotation.getData(),
                                     load_vec.getData())

        Logger.log(
            "d", "Smart Slice {} Vector: {}".format(self.getName(),
                                                    rotated_load_vec))

        force.force.set([
            float(rotated_load_vec[0]),
            float(rotated_load_vec[1]),
            float(rotated_load_vec[2])
        ])

        if self.force.pull:
            arrow_start = self._rotator.center
        else:
            arrow_start = self.activeArrow.tailPosition
        rotated_start = numpy.dot(mesh_rotation.getData(),
                                  arrow_start.getData())

        if self.axis:
            force.origin.set([
                float(rotated_start[0]),
                float(rotated_start[1]),
                float(rotated_start[2]),
            ])

        # Add the face Ids from the STL mesh that the user selected for this force
        force.face.extend(self.getTriangleIndices())

        Logger.log(
            "d", "Smart Slice {} Triangles: {}".format(self.getName(),
                                                       force.face))

        step.loads.append(force)

        return force
예제 #9
0
 def test_preMultiply(self):
     temp_matrix = Matrix()
     temp_matrix.setByTranslation(Vector(10, 10, 10))
     temp_matrix2 = Matrix()
     temp_matrix2.setByScaleFactor(0.5)
     temp_matrix.preMultiply(temp_matrix2)
     numpy.testing.assert_array_almost_equal(
         temp_matrix.getData(),
         numpy.array([[0.5, 0, 0, 5], [0, 0.5, 0, 5], [0, 0, 0.5, 5],
                      [0, 0, 0, 1]]))
예제 #10
0
    def addDonut(self, inner_radius, outer_radius, width, center = Vector(0, 0, 0), sections = 32, color = None, angle = 0, axis = Vector.Unit_Y):
        vertices = []
        indices = []
        colors = []

        start = self.getVertexCount() #Starting index.

        for i in range(sections):
            v1 = start + i * 3 #Indices for each of the vertices we'll add for this section.
            v2 = v1 + 1
            v3 = v1 + 2
            v4 = v1 + 3
            v5 = v1 + 4
            v6 = v1 + 5

            if i+1 >= sections: # connect the end to the start
                v4 = start
                v5 = start + 1
                v6 = start + 2

            theta = i * math.pi / (sections / 2) #Angle of this piece around torus perimeter.
            c = math.cos(theta) #X-coordinate around torus perimeter.
            s = math.sin(theta) #Y-coordinate around torus perimeter.

            #One vertex on the inside perimeter, two on the outside perimiter (up and down).
            vertices.append( [inner_radius * c, inner_radius * s, 0] )
            vertices.append( [outer_radius * c, outer_radius * s, width] )
            vertices.append( [outer_radius * c, outer_radius * s, -width] )

            #Connect the vertices to the next segment.
            indices.append( [v1, v4, v5] )
            indices.append( [v2, v1, v5] )

            indices.append( [v2, v5, v6] )
            indices.append( [v3, v2, v6] )

            indices.append( [v3, v6, v4] )
            indices.append( [v1, v3, v4] )

            if color: #If we have a colour, add it to the vertices.
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )

        #Rotate the resulting torus around the specified axis.
        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        vertices = numpy.asarray(vertices, dtype = numpy.float32)
        vertices = vertices.dot(matrix.getData()[0:3, 0:3])
        vertices[:] += center.getData() #And translate to the desired position.

        self.addVertices(vertices)
        self.addIndices(numpy.asarray(indices, dtype = numpy.int32))
        self.addColors(numpy.asarray(colors, dtype = numpy.float32))
예제 #11
0
    def addDonut(self, inner_radius, outer_radius, width, center = Vector(0, 0, 0), sections = 32, color = None, angle = 0, axis = Vector.Unit_Y):
        vertices = []
        indices = []
        colors = []

        start = self.getVertexCount() #Starting index.

        for i in range(sections):
            v1 = start + i * 3 #Indices for each of the vertices we'll add for this section.
            v2 = v1 + 1
            v3 = v1 + 2
            v4 = v1 + 3
            v5 = v1 + 4
            v6 = v1 + 5

            if i+1 >= sections: # connect the end to the start
                v4 = start
                v5 = start + 1
                v6 = start + 2

            theta = i * math.pi / (sections / 2) #Angle of this piece around torus perimeter.
            c = math.cos(theta) #X-coordinate around torus perimeter.
            s = math.sin(theta) #Y-coordinate around torus perimeter.

            #One vertex on the inside perimeter, two on the outside perimiter (up and down).
            vertices.append( [inner_radius * c, inner_radius * s, 0] )
            vertices.append( [outer_radius * c, outer_radius * s, width] )
            vertices.append( [outer_radius * c, outer_radius * s, -width] )

            #Connect the vertices to the next segment.
            indices.append( [v1, v4, v5] )
            indices.append( [v2, v1, v5] )

            indices.append( [v2, v5, v6] )
            indices.append( [v3, v2, v6] )

            indices.append( [v3, v6, v4] )
            indices.append( [v1, v3, v4] )

            if color: #If we have a colour, add it to the vertices.
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )

        #Rotate the resulting torus around the specified axis.
        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        vertices = numpy.asarray(vertices, dtype = numpy.float32)
        vertices = vertices.dot(matrix.getData()[0:3, 0:3])
        vertices[:] += center.getData() #And translate to the desired position.

        self.addVertices(vertices)
        self.addIndices(numpy.asarray(indices, dtype = numpy.int32))
        self.addColors(numpy.asarray(colors, dtype = numpy.float32))
예제 #12
0
def transformVertices(vertices: numpy.ndarray, transformation: Matrix) -> numpy.ndarray:
    """Transform an array of vertices using a matrix

    :param vertices: :type{numpy.ndarray} array of 3D vertices
    :param transformation: a 4x4 matrix
    :return: :type{numpy.ndarray} the transformed vertices
    """

    data = numpy.pad(vertices, ((0, 0), (0, 1)), "constant", constant_values=(0.0, 0.0))
    data = data.dot(transformation.getTransposed().getData())
    data += transformation.getData()[:, 3]
    data = data[:, 0:3]
    return data
예제 #13
0
    def addPyramid(self,
                   width,
                   height,
                   depth,
                   angle=0,
                   axis=Vector.Unit_Y,
                   center=Vector(0, 0, 0),
                   color=None):
        angle = math.radians(angle)

        minW = -width / 2
        maxW = width / 2
        minD = -depth / 2
        maxD = depth / 2

        start = self.getVertexCount()  #Starting index.

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        verts = numpy.asarray(
            [  #All 5 vertices of the pyramid.
                [minW, 0, maxD], [maxW, 0, maxD], [minW, 0, minD],
                [maxW, 0, minD], [0, height, 0]
            ],
            dtype=numpy.float32)
        verts = verts.dot(
            matrix.getData()[0:3, 0:3])  #Rotate the pyramid around the axis.
        verts[:] += center.getData()
        self.addVertices(verts)

        indices = numpy.asarray(
            [  #Connect the vertices to each other (6 triangles).
                [start, start + 1, start + 4],  #The four sides of the pyramid.
                [start + 1, start + 3, start + 4],
                [start + 3, start + 2, start + 4],
                [start + 2, start, start + 4],
                [start, start + 3, start + 1],  #The base of the pyramid.
                [start, start + 2, start + 3]
            ],
            dtype=numpy.int32)
        self.addIndices(indices)

        if color:  #If we have a colour, add the colour to each of the vertices.
            vertex_count = self.getVertexCount()
            for i in range(1, 6):
                self.setVertexColor(vertex_count - i, color)
예제 #14
0
    def addPyramid(self, **kwargs):
        width = kwargs["width"]
        height = kwargs["height"]
        depth = kwargs["depth"]

        angle = math.radians(kwargs.get("angle", 0))
        axis = kwargs.get("axis", Vector.Unit_Y)

        center = kwargs.get("center", Vector(0, 0, 0))

        minW = -width / 2
        maxW = width / 2
        minD = -depth / 2
        maxD = depth / 2

        start = self._mesh_data.getVertexCount()

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        verts = numpy.asarray([
            [minW, 0, maxD],
            [maxW, 0, maxD],
            [minW, 0, minD],
            [maxW, 0, minD],
            [0, height, 0]
        ], dtype=numpy.float32)
        verts = verts.dot(matrix.getData()[0:3,0:3])
        verts[:] += center.getData()
        self._mesh_data.addVertices(verts)

        indices = numpy.asarray([
            [start, start + 1, start + 4],
            [start + 1, start + 3, start + 4],
            [start + 3, start + 2, start + 4],
            [start + 2, start, start + 4],
            [start, start + 3, start + 1],
            [start, start + 2, start + 3]
        ], dtype=numpy.int32)
        self._mesh_data.addIndices(indices)

        color = kwargs.get("color", None)
        if color:
            vertex_count = self._mesh_data.getVertexCount()
            for i in range(1, 6):
                self._mesh_data.setVertexColor(vertex_count - i, color)
예제 #15
0
def transformNormals(normals: numpy.ndarray, transformation: Matrix) -> numpy.ndarray:
    data = numpy.pad(normals, ((0, 0), (0, 1)), "constant", constant_values=(0.0, 0.0))

    # Get the translation from the transformation so we can cancel it later.
    translation = transformation.getTranslation()

    # Transform the normals so they get the proper rotation
    data = data.dot(transformation.getTransposed().getData())
    data += transformation.getData()[:, 3]
    data = data[:, 0:3]

    # Cancel the translation since normals should always go from origin to a point on the unit sphere.
    data[:] -= translation.getData()

    # Re-normalize the normals, since the transformation can contain scaling.
    lengths = numpy.linalg.norm(data, axis = 1)
    data[:, 0] /= lengths
    data[:, 1] /= lengths
    data[:, 2] /= lengths

    return data
예제 #16
0
def transformNormals(normals: numpy.ndarray, transformation: Matrix) -> numpy.ndarray:
    data = numpy.pad(normals, ((0, 0), (0, 1)), "constant", constant_values=(0.0, 0.0))

    # Get the translation from the transformation so we can cancel it later.
    translation = transformation.getTranslation()

    # Transform the normals so they get the proper rotation
    data = data.dot(transformation.getTransposed().getData())
    data += transformation.getData()[:, 3]
    data = data[:, 0:3]

    # Cancel the translation since normals should always go from origin to a point on the unit sphere.
    data[:] -= translation.getData()

    # Re-normalize the normals, since the transformation can contain scaling.
    lengths = numpy.linalg.norm(data, axis = 1)
    data[:, 0] /= lengths
    data[:, 1] /= lengths
    data[:, 2] /= lengths

    return data
예제 #17
0
    def addPyramid(self, width, height, depth, angle = 0, axis = Vector.Unit_Y, center = Vector(0, 0, 0), color = None):
        angle = math.radians(angle)

        minW = -width / 2
        maxW = width / 2
        minD = -depth / 2
        maxD = depth / 2

        start = self.getVertexCount() #Starting index.

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        verts = numpy.asarray([ #All 5 vertices of the pyramid.
            [minW, 0, maxD],
            [maxW, 0, maxD],
            [minW, 0, minD],
            [maxW, 0, minD],
            [0, height, 0]
        ], dtype=numpy.float32)
        verts = verts.dot(matrix.getData()[0:3,0:3]) #Rotate the pyramid around the axis.
        verts[:] += center.getData()
        self.addVertices(verts)

        indices = numpy.asarray([ #Connect the vertices to each other (6 triangles).
            [start, start + 1, start + 4], #The four sides of the pyramid.
            [start + 1, start + 3, start + 4],
            [start + 3, start + 2, start + 4],
            [start + 2, start, start + 4],
            [start, start + 3, start + 1], #The base of the pyramid.
            [start, start + 2, start + 3]
        ], dtype=numpy.int32)
        self.addIndices(indices)

        if color: #If we have a colour, add the colour to each of the vertices.
            vertex_count = self.getVertexCount()
            for i in range(1, 6):
                self.setVertexColor(vertex_count - i, color)
예제 #18
0
    def _rotateCamera(self, yaw: float, pitch: float, roll: float) -> None:
        camera = self._scene.getActiveCamera()
        if not camera or not camera.isEnabled():
            return

        dyaw = math.radians(yaw * 180.0)
        dpitch = math.radians(pitch * 180.0)
        droll = math.radians(roll * 180.0)

        diff = camera.getPosition() - self._camera_tool._origin

        myaw = Matrix()
        myaw.setByRotationAxis(dyaw, Vector.Unit_Y)

        mpitch = Matrix(myaw.getData())
        mpitch.rotateByAxis(dpitch, Vector.Unit_Y.cross(diff))

        n = diff.multiply(mpitch)

        try:
            angle = math.acos(Vector.Unit_Y.dot(n.normalized()))
        except ValueError:
            return

        if angle < 0.1 or angle > math.pi - 0.1:
            n = diff.multiply(myaw)

        n += self._camera_tool._origin

        camera.setPosition(n)

        if abs(self._roll + droll) < math.pi * 0.45:
            self._roll += droll
        mroll = Matrix()
        mroll.setByRotationAxis(self._roll, (n - self._camera_tool._origin))
        camera.lookAt(self._camera_tool._origin, Vector.Unit_Y.multiply(mroll))
예제 #19
0
    def checkQueuedNodes(self) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if global_container_stack:
            disallowed_edge = self._application.getBuildVolume().getEdgeDisallowedSize() + 2  # Allow for some rounding errors
            max_x_coordinate = (global_container_stack.getProperty("machine_width", "value") / 2) - disallowed_edge
            max_y_coordinate = (global_container_stack.getProperty("machine_depth", "value") / 2) - disallowed_edge

        for node in self._node_queue:
            mesh_data = node.getMeshData()
            if not mesh_data:
                continue
            file_name = mesh_data.getFileName()

            if self._preferences.getValue("meshtools/randomise_location_on_load") and global_container_stack != None:
                if file_name and os.path.splitext(file_name)[1].lower() == ".3mf": # don't randomise project files
                    continue

                node_bounds = node.getBoundingBox()
                position = self._randomLocation(node_bounds, max_x_coordinate, max_y_coordinate)
                node.setPosition(position)

            if (
                self._preferences.getValue("meshtools/check_models_on_load") or
                self._preferences.getValue("meshtools/fix_normals_on_load") or
                self._preferences.getValue("meshtools/model_unit_factor") != 1
            ):

                tri_node = self._toTriMesh(mesh_data)

            if self._preferences.getValue("meshtools/model_unit_factor") != 1:
                if file_name and os.path.splitext(file_name)[1].lower() not in [".stl", ".obj", ".ply"]:
                    # only resize models that don't have an intrinsic unit set
                    continue

                scale_matrix = Matrix()
                scale_matrix.setByScaleFactor(float(self._preferences.getValue("meshtools/model_unit_factor")))
                tri_node.apply_transform(scale_matrix.getData())

                self._replaceSceneNode(node, [tri_node])

            if self._preferences.getValue("meshtools/check_models_on_load") and not tri_node.is_watertight:
                if not file_name:
                    file_name = catalog.i18nc("@text Print job name", "Untitled")
                base_name = os.path.basename(file_name)

                if file_name in self._mesh_not_watertight_messages:
                    self._mesh_not_watertight_messages[file_name].hide()

                message = Message(title=catalog.i18nc("@info:title", "Mesh Tools"))
                body = catalog.i18nc("@info:status", "Model %s is not watertight, and may not print properly.") % base_name

                # XRayView may not be available if the plugin has been disabled
                active_view = self._controller.getActiveView()
                if active_view and "XRayView" in self._controller.getAllViews() and active_view.getPluginId() != "XRayView":
                    body += " " + catalog.i18nc("@info:status", "Check X-Ray View and repair the model before printing it.")
                    message.addAction("X-Ray", catalog.i18nc("@action:button", "Show X-Ray View"), "", "")
                    message.actionTriggered.connect(self._showXRayView)
                else:
                    body += " " +catalog.i18nc("@info:status", "Repair the model before printing it.")

                message.setText(body)
                message.show()

                self._mesh_not_watertight_messages[file_name] = message

            if self._preferences.getValue("meshtools/fix_normals_on_load") and tri_node.is_watertight:
                tri_node.fix_normals()
                self._replaceSceneNode(node, [tri_node])

        self._node_queue = []
예제 #20
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)

    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 Matrix(self._projection_matrix.getData())

    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 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

    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().getInverse()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)
        return position.x / position.z / 2.0, position.y / position.z / 2.0
예제 #21
0
파일: X3DReader.py 프로젝트: cederom/Cura
class X3DReader(MeshReader):
    def __init__(self):
        super().__init__()
        self._supported_extensions = [".x3d"]
        self._namespaces = {}
    
    # Main entry point
    # Reads the file, returns a SceneNode (possibly with nested ones), or None
    def read(self, file_name):
        try:
            self.defs = {}
            self.shapes = []
            
            tree = ET.parse(file_name)
            xml_root = tree.getroot()
            
            if xml_root.tag != "X3D":
                return None

            scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters            
            if xml_root[0].tag == "head":
                for head_node in xml_root[0]:
                    if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
                        scale *= float(head_node.attrib["conversionFactor"])
                        break 
                xml_scene = xml_root[1]
            else:
                xml_scene = xml_root[0]
                
            if xml_scene.tag != "Scene":
                return None
            
            self.transform = Matrix()
            self.transform.setByScaleFactor(scale)
            self.index_base = 0
            
            # Traverse the scene tree, populate the shapes list
            self.processChildNodes(xml_scene)
            
            if self.shapes:
                builder = MeshBuilder()
                builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
                builder.setIndices(numpy.concatenate([shape.faces for shape in self.shapes]))
                builder.calculateNormals()
                builder.setFileName(file_name)
                mesh_data = builder.build()

                # Manually try and get the extents of the mesh_data. This should prevent nasty NaN issues from
                # leaving the reader.
                mesh_data.getExtents()

                node = SceneNode()
                node.setMeshData(mesh_data)
                node.setSelectable(True)
                node.setName(file_name)

            else:
                return None
            
        except Exception:
            Logger.logException("e", "Exception in X3D reader")
            return None

        return node
    
    # ------------------------- XML tree traversal
  
    def processNode(self, xml_node):
        xml_node =  self.resolveDefUse(xml_node)
        if xml_node is None:
            return
        
        tag = xml_node.tag
        if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
            self.processChildNodes(xml_node)
        if tag == "CADPart":
            self.processTransform(xml_node) # TODO: split the parts
        elif tag == "LOD":
            self.processNode(xml_node[0])
        elif tag == "Transform":
            self.processTransform(xml_node)
        elif tag == "Shape":
            self.processShape(xml_node)
            
            
    def processShape(self, xml_node):
        # Find the geometry and the appearance inside the Shape
        geometry = appearance = None
        for sub_node in xml_node:
            if sub_node.tag == "Appearance" and not appearance:
                appearance = self.resolveDefUse(sub_node)
            elif sub_node.tag in self.geometry_importers and not geometry:
                geometry = self.resolveDefUse(sub_node)
        
        # TODO: appearance is completely ignored. At least apply the material color...        
        if not geometry is None:
            try:
                self.verts = self.faces = [] # Safeguard 
                self.geometry_importers[geometry.tag](self, geometry)
                m = self.transform.getData()
                verts = m.dot(self.verts)[:3].transpose()
                
                self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
                self.index_base += len(verts)
                
            except Exception:
                Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
        
    # Returns the referenced node if the node has USE, the same node otherwise.
    # May return None is USE points at a nonexistent node
    # In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
    # Big caveat: XML element objects may evaluate to boolean False!!!
    # Don't ever use "if node:", use "if not node is None:" instead
    def resolveDefUse(self, node):
        USE = node.attrib.get("USE")
        if USE:
            return self.defs.get(USE, None)

        DEF = node.attrib.get("DEF")            
        if DEF:
            self.defs[DEF] = node 
        return node
    
    def processChildNodes(self, node):
        for c in node:
            self.processNode(c)
            Job.yieldThread()
    
    # Since this is a grouping node, will recurse down the tree.
    # According to the spec, the final transform matrix is:
    # T * C * R * SR * S * -SR * -C
    # Where SR corresponds to the rotation matrix to scaleOrientation
    # C and SR are rather exotic. S, slightly less so. 
    def processTransform(self, node):
        rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
        trans = readVector(node, "translation", (0, 0, 0)) # Vector
        scale = readVector(node, "scale", (1, 1, 1)) # Vector
        center = readVector(node, "center", (0, 0, 0)) # Vector
        scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
        
        # Store the previous transform; in Cura, the default matrix multiplication is in place        
        prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
        
        # The rest of transform manipulation will be applied in place
        got_center = (center.x != 0 or center.y != 0 or center.z != 0)
        
        T = self.transform
        if trans.x != 0 or trans.y != 0 or trans.z !=0:
            T.translate(trans)
        if got_center:
            T.translate(center)
        if rot[0] != 0:
            T.rotateByAxis(*rot)
        if scale.x != 1 or scale.y != 1 or scale.z != 1:
            got_scale_orient = scale_orient[0] != 0
            if got_scale_orient:
                T.rotateByAxis(*scale_orient)
            # No scale by vector in place operation in UM
            S = Matrix()
            S.setByScaleVector(scale)
            T.multiply(S)
            if got_scale_orient:
                T.rotateByAxis(-scale_orient[0], scale_orient[1])
        if got_center:
            T.translate(-center)
            
        self.processChildNodes(node)
        self.transform = prev
    
    # ------------------------- Geometry importers
    # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
    
    # Primitives

    def processGeometryBox(self, node):
        (dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
        dx /= 2
        dy /= 2
        dz /= 2
        self.reserveFaceAndVertexCount(12, 8)

        # xz plane at +y, ccw
        self.addVertex(dx, dy, dz)
        self.addVertex(-dx, dy, dz)
        self.addVertex(-dx, dy, -dz)
        self.addVertex(dx, dy, -dz)
        # xz plane at -y
        self.addVertex(dx, -dy, dz)
        self.addVertex(-dx, -dy, dz)
        self.addVertex(-dx, -dy, -dz)
        self.addVertex(dx, -dy, -dz)
        
        self.addQuad(0, 1, 2, 3)   # +y
        self.addQuad(4, 0, 3, 7)   # +x
        self.addQuad(7, 3, 2, 6)   # -z
        self.addQuad(6, 2, 1, 5)   # -x
        self.addQuad(5, 1, 0, 4)   # +z
        self.addQuad(7, 6, 5, 4)  # -y
    
    # The sphere is subdivided into nr rings and ns segments
    def processGeometrySphere(self, node):
        r = readFloat(node, "radius", 0.5)
        subdiv = readIntArray(node, "subdivision", None)
        if subdiv:
            if len(subdiv) == 1:
                nr = ns = subdiv[0]
            else:
                (nr, ns) = subdiv
        else:
            nr = ns = DEFAULT_SUBDIV
            
        lau = pi / nr  # Unit angle of latitude (rings) for the given tesselation
        lou = 2 * pi / ns  # Unit angle of longitude (segments)
        
        self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
        
        # +y and -y poles
        self.addVertex(0, r, 0)
        self.addVertex(0, -r, 0)
        
        # The non-polar vertices go from x=0, negative z plane counterclockwise -
        # to -x, to +z, to +x, back to -z
        for ring in range(1, nr):
            for seg in range(ns):
                self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
                          r*cos(lau * ring),
                          -r*cos(lou * seg) * sin(lau * ring))
                
        vb = 2 + (nr - 2) * ns  # First vertex index for the bottom cap
    
        # Faces go in order: top cap, sides, bottom cap.
        # Sides go by ring then by segment.
    
        # Caps
        # Top cap face vertices go in order: down right up
        # (starting from +y pole)
        # Bottom cap goes: up left down (starting from -y pole)
        for seg in range(ns):
            self.addTri(0, seg + 2, (seg + 1) % ns + 2)
            self.addTri(1, vb + (seg + 1) % ns, vb + seg)
    
        # Sides
        # Side face vertices go in order:  down right upleft, downright up left
        for ring in range(nr - 2):
            tvb = 2 + ring * ns
            # First vertex index for the top edge of the ring
            bvb = tvb + ns
            # First vertex index for the bottom edge of the ring
            for seg in range(ns):
                nseg = (seg + 1) % ns
                self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
        
    def processGeometryCone(self, node):
        r = readFloat(node, "bottomRadius", 1)
        height = readFloat(node, "height", 2)
        bottom = readBoolean(node, "bottom", True)
        side = readBoolean(node, "side", True)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)
        
        d = height / 2
        angle = 2 * pi / n
    
        self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
    
        # Vertex 0 is the apex, vertices 1..n are the bottom
        self.addVertex(0, d, 0)
        for i in range(n):
            self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
                    
        # Side face vertices go: up down right
        if side:
            for i in range(n):
                self.addTri(1 + (i + 1) % n, 0, 1 + i)
        if bottom:
            for i in range(2, n):
                self.addTri(1, i, i+1)
    
    def processGeometryCylinder(self, node):
        r = readFloat(node, "radius", 1)
        height = readFloat(node, "height", 2)
        bottom = readBoolean(node, "bottom", True)
        side = readBoolean(node, "side", True)
        top = readBoolean(node, "top", True)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)
        
        nn = n * 2
        angle = 2 * pi / n
        hh = height/2
        
        self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
        
        # The seam is at x=0, z=-r, vertices go ccw -
        # to pos x, to neg z, to neg x, back to neg z
        for i in range(n):
            rs = -r * sin(angle * i)
            rc = -r * cos(angle * i)
            self.addVertex(rs, hh, rc)
            self.addVertex(rs, -hh, rc)
        
        if side:
            for i in range(n):
                ni = (i + 1) % n
                self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
            
        for i in range(2, nn-3, 2):
            if top:
                self.addTri(0, i, i+2)
            if bottom:
                self.addTri(1, i+1, i+3)
    
    # Semi-primitives

    def processGeometryElevationGrid(self, node):
        dx = readFloat(node, "xSpacing", 1)
        dz = readFloat(node, "zSpacing", 1)
        nx = readInt(node, "xDimension", 0)
        nz = readInt(node, "zDimension", 0)
        height = readFloatArray(node, "height", False)
        ccw = readBoolean(node, "ccw", True)
        
        if nx <= 0 or nz <= 0 or len(height) < nx*nz:
            return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
        
        self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
        
        for z in range(nz):
            for x in range(nx):
                self.addVertex(x * dx, height[z*nx + x], z * dz)
                
        for z in range(1, nz):
            for x in range(1, nx):
                self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
                self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
    
    def processGeometryExtrusion(self, node):
        ccw = readBoolean(node, "ccw", True)
        begin_cap = readBoolean(node, "beginCap", True)
        end_cap = readBoolean(node, "endCap", True)
        cross = readFloatArray(node, "crossSection", (1, 1, 1, -1, -1, -1, -1, 1, 1, 1))
        cross = [(cross[i], cross[i+1]) for i in range(0, len(cross), 2)]
        spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0))
        spine = [(spine[i], spine[i+1], spine[i+2]) for i in range(0, len(spine), 3)]
        orient = readFloatArray(node, "orientation", None)
        if orient:
            # This converts X3D's axis/angle rotation to a 3x3 numpy matrix
            def toRotationMatrix(rot):
                (x, y, z) = rot[:3]
                a = rot[3]  
                s = sin(a)
                c = cos(a)
                t = 1-c
                return numpy.array((
                    (x * x * t + c,  x * y * t - z*s, x * z * t + y * s),
                    (x * y * t + z*s, y * y * t + c, y * z * t - x * s),
                    (x * z * t - y * s, y * z * t + x * s, z * z * t + c)))   
            
            orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
            
        scale = readFloatArray(node, "scale", None)
        if scale:
            scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
                     if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
        
        
        # Special treatment for the closed spine and cross section.
        # Let's save some memory by not creating identical but distinct vertices;
        # later we'll introduce conditional logic to link the last vertex with
        # the first one where necessary.
        crossClosed = cross[0] == cross[-1]
        if crossClosed:
            cross = cross[:-1]
        nc = len(cross)
        cross = [numpy.array((c[0], 0, c[1])) for c in cross]
        ncf = nc if crossClosed else nc - 1
        # Face count along the cross; for closed cross, it's the same as the
        # respective vertex count
    
        spine_closed = spine[0] == spine[-1]
        if spine_closed:
            spine = spine[:-1]
        ns = len(spine)
        spine = [Vector(*s) for s in spine]
        nsf = ns if spine_closed else ns - 1
    
        # This will be used for fallback, where the current spine point joins
        # two collinear spine segments. No need to recheck the case of the
        # closed spine/last-to-first point juncture; if there's an angle there,
        # it would kick in on the first iteration of the main loop by spine.
        def findFirstAngleNormal():
            for i in range(1, ns - 1):
                spt = spine[i]
                z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
                if z.length() > EPSILON:
                    return z
            # All the spines are collinear. Fallback to the rotated source
            # XZ plane.
            # TODO: handle the situation where the first two spine points match
            if len(spine) < 2:
                return Vector(0, 0, 1)
            v = spine[1] - spine[0]
            orig_y = Vector(0, 1, 0)
            orig_z = Vector(0, 0, 1)
            if v.cross(orig_y).length() > EPSILON:
                # Spine at angle with global y - rotate the z accordingly
                a = v.cross(orig_y) # Axis of rotation to get to the Z
                (x, y, z) = a.normalized().getData()  
                s = a.length()/v.length()
                c = sqrt(1-s*s)
                t = 1-c
                m = numpy.array((
                    (x * x * t + c,  x * y * t + z*s, x * z * t - y * s),
                    (x * y * t - z*s, y * y * t + c, y * z * t + x * s),
                    (x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
                orig_z = Vector(*m.dot(orig_z.getData()))
            return orig_z
    
        self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)

        z = None
        for i, spt in enumerate(spine):
            if (i > 0 and i < ns - 1) or spine_closed:
                snext = spine[(i + 1) % ns]
                sprev = spine[(i - 1 + ns) % ns]
                y = snext - sprev
                vnext = snext - spt
                vprev = sprev - spt
                try_z = vnext.cross(vprev)
                # Might be zero, then all kinds of fallback
                if try_z.length() > EPSILON:
                    if z is not None and try_z.dot(z) < 0:
                        try_z = -try_z
                    z = try_z
                elif not z:  # No z, and no previous z.
                    # Look ahead, see if there's at least one point where
                    # spines are not collinear.
                    z = findFirstAngleNormal()
            elif i == 0:  # And non-crossed
                snext = spine[i + 1]
                y = snext - spt
                z = findFirstAngleNormal()
            else:  # last point and not crossed
                sprev = spine[i - 1]
                y = spt - sprev
                # If there's more than one point in the spine, z is already set.
                # One point in the spline is an error anyway.
    
            z = z.normalized()
            y = y.normalized()
            x = y.cross(z) # Already normalized
            m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
            
            # Columns are the unit vectors for the xz plane for the cross-section
            if orient:
                mrot = orient[i] if len(orient) > 1 else orient[0]
                if not mrot is None:
                    m = m.dot(mrot)  # Tested against X3DOM, the result matches, still not sure :(
                    
            if scale:
                mscale = scale[i] if len(scale) > 1 else scale[0]
                if not mscale is None:
                    m = m.dot(mscale)
                    
            # First the cross-section 2-vector is scaled,
            # then rotated (which may make it a 3-vector),
            # then applied to the xz plane unit vectors
                    
            sptv3 = numpy.array(spt.getData()[:3])
            for cpt in cross:
                v = sptv3 + m.dot(cpt)
                self.addVertex(*v)
    
        if begin_cap:
            self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
    
        # Order of edges in the face: forward along cross, forward along spine,
        # backward along cross, backward along spine, flipped if now ccw.
        # This order is assumed later in the texture coordinate assignment;
        # please don't change without syncing.
    
        for s in range(ns - 1):
            for c in range(ncf):
                self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
                    (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
    
        if spine_closed:
            # The faces between the last and the first spine points
            b = (ns - 1) * nc
            for c in range(ncf):
                self.addQuadFlip(b + c, b + (c + 1) % nc,
                    (c + 1) % nc, c, ccw)
                        
        if end_cap:
            self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
    
# Triangle meshes

    # Helper for numerous nodes with a Coordinate subnode holding vertices
    # That all triangle meshes and IndexedFaceSet
    # num_faces can be a function, in case the face count is a function of vertex count 
    def startCoordMesh(self, node, num_faces):
        ccw = readBoolean(node, "ccw", True)
        self.readVertices(node) # This will allocate and fill the vertex array
        if hasattr(num_faces, "__call__"):
            num_faces = num_faces(self.getVertexCount())
        self.reserveFaceCount(num_faces)
            
        return ccw
        

    def processGeometryIndexedTriangleSet(self, node):
        index = readIntArray(node, "index", [])
        num_faces = len(index) // 3
        ccw = int(self.startCoordMesh(node, num_faces))
        
        for i in range(0, num_faces*3, 3):
            self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
    
    def processGeometryIndexedTriangleStripSet(self, node):
        strips = readIndex(node, "index")
        ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
            
        for strip in strips:
            sccw = ccw # Running CCW value, reset for each strip
            for i in range(len(strip) - 2):
                self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
                sccw = 1 - sccw
    
    def processGeometryIndexedTriangleFanSet(self, node):
        fans = readIndex(node, "index")
        ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
        
        for fan in fans:
            for i in range(1, len(fan) - 1):
                self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
   
    def processGeometryTriangleSet(self, node):
        ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
        for i in range(0, self.getVertexCount(), 3):
            self.addTri(i + 1 - ccw, i + ccw, i+2)
    
    def processGeometryTriangleStripSet(self, node):
        strips = readIntArray(node, "stripCount", [])
        ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
            
        vb = 0
        for n in strips:
            sccw = ccw
            for i in range(n-2): 
                self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
                sccw = 1 - sccw
            vb += n
    
    def processGeometryTriangleFanSet(self, node):
        fans = readIntArray(node, "fanCount", [])
        ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
        
        vb = 0
        for n in fans:
            for i in range(1, n-1): 
                self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
            vb += n
            
    # Quad geometries from the CAD module, might be relevant for printing
    
    def processGeometryQuadSet(self, node):
        ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
        for i in range(0, self.getVertexCount(), 4):
            self.addQuadFlip(i, i+1, i+2, i+3, ccw)
            
    def processGeometryIndexedQuadSet(self, node):
        index = readIntArray(node, "index", [])
        num_quads = len(index) // 4
        ccw = self.startCoordMesh(node, num_quads*2)
        
        for i in range(0, num_quads*4, 4):
            self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
            
    # 2D polygon geometries
    # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
    # The only way around that is merging meshes.
    
    def processGeometryDisk2D(self, node):
        innerRadius = readFloat(node, "innerRadius", 0)
        outerRadius = readFloat(node, "outerRadius", 1)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)
        
        angle = 2 * pi / n
        
        self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
            
        for i in range(n):
            s = sin(angle * i)
            c = cos(angle * i)
            self.addVertex(outerRadius*c, outerRadius*s, 0)
            if innerRadius:
                self.addVertex(innerRadius*c, innerRadius*s, 0)
                ni = (i+1) % n
                self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
                
        if not innerRadius:
            for i in range(2, n):
                self.addTri(0, i-1, i)
                
    def processGeometryRectangle2D(self, node):
        (x, y) = readFloatArray(node, "size", (2, 2))
        self.reserveFaceAndVertexCount(2, 4)
        self.addVertex(-x/2, -y/2, 0)
        self.addVertex(x/2, -y/2, 0)
        self.addVertex(x/2, y/2, 0)
        self.addVertex(-x/2, y/2, 0)
        self.addQuad(0, 1, 2, 3)
        
    def processGeometryTriangleSet2D(self, node):
        verts = readFloatArray(node, "vertices", ())
        num_faces = len(verts) // 6;
        verts = [(verts[i], verts[i+1], 0) for i in range(0, 6 * num_faces, 2)]
        self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
        for vert in verts:
            self.addVertex(*vert)
        
        # The front face is on the +Z side, so CCW is a variable
        for i in range(0, num_faces*3, 3):
            a = Vector(*verts[i+2]) - Vector(*verts[i])
            b = Vector(*verts[i+1]) - Vector(*verts[i])
            self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
    
    # General purpose polygon mesh

    def processGeometryIndexedFaceSet(self, node):
        faces = readIndex(node, "coordIndex")
        ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
            
        for face in faces:
            if len(face) == 3:
                self.addTriFlip(face[0], face[1], face[2], ccw)
            elif len(face) > 3:
                self.addFace(face, ccw)
                
    geometry_importers = {
        "IndexedFaceSet": processGeometryIndexedFaceSet,
        "IndexedTriangleSet": processGeometryIndexedTriangleSet,
        "IndexedTriangleStripSet": processGeometryIndexedTriangleStripSet,
        "IndexedTriangleFanSet": processGeometryIndexedTriangleFanSet,
        "TriangleSet": processGeometryTriangleSet,
        "TriangleStripSet": processGeometryTriangleStripSet,
        "TriangleFanSet": processGeometryTriangleFanSet,
        "QuadSet": processGeometryQuadSet,
        "IndexedQuadSet": processGeometryIndexedQuadSet,
        "TriangleSet2D": processGeometryTriangleSet2D,
        "Rectangle2D": processGeometryRectangle2D,
        "Disk2D": processGeometryDisk2D,
        "ElevationGrid": processGeometryElevationGrid,
        "Extrusion": processGeometryExtrusion,
        "Sphere": processGeometrySphere,
        "Box": processGeometryBox,
        "Cylinder": processGeometryCylinder,
        "Cone": processGeometryCone
    }
    
    # Parses the Coordinate.@point field, fills the verts array.
    def readVertices(self, node):
        for c in node:
            if c.tag == "Coordinate":
                c = self.resolveDefUse(c)
                if not c is None:
                    pt = c.attrib.get("point")
                    if pt:
                        co = [float(x) for x in pt.split()]
                        num_verts = len(co) // 3
                        self.verts = numpy.empty((4, num_verts), dtype=numpy.float32)
                        self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
                        # Group by three
                        for i in range(num_verts):
                            self.verts[:3,i] = co[3*i:3*i+3]
    
    # Mesh builder helpers
    
    def reserveFaceAndVertexCount(self, num_faces, num_verts):
        # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
        self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
        self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
        self.num_verts = 0
        self.reserveFaceCount(num_faces)
        
    def reserveFaceCount(self, num_faces):
        self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
        self.num_faces = 0
        
    def getVertexCount(self):
        return self.verts.shape[1]
        
    def addVertex(self, x, y, z):
        self.verts[0, self.num_verts] = x
        self.verts[1, self.num_verts] = y
        self.verts[2, self.num_verts] = z
        self.num_verts += 1
    
    # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
    def addTri(self, a, b, c):
        self.faces[self.num_faces, 0] = self.index_base + a
        self.faces[self.num_faces, 1] = self.index_base + b
        self.faces[self.num_faces, 2] = self.index_base + c
        self.num_faces += 1
        
    def addTriFlip(self, a, b, c, ccw):
        if ccw:
            self.addTri(a, b, c)
        else:
            self.addTri(b, a, c)
        
    # Needs to be convex, but not necessaily planar
    # Assumed ccw, cut along the ac diagonal
    def addQuad(self, a, b, c, d):
        self.addTri(a, b, c)
        self.addTri(c, d, a)
        
    def addQuadFlip(self, a, b, c, d, ccw):
        if ccw:
            self.addTri(a, b, c)
            self.addTri(c, d, a)
        else:
            self.addTri(a, c, b)
            self.addTri(c, a, d)    
    
    
    # Arbitrary polygon triangulation.
    # Doesn't assume convexity and doesn't check the "convex" flag in the file.
    # Works by the "cutting of ears" algorithm:
    # - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
    # - Remove the triangle at that vertex
    # - Repeat until done
    # Vertex coordinates are supposed to be already set
    def addFace(self, indices, ccw):
        # Resolve indices to coordinates for faster math
        face = [Vector(data=self.verts[0:3, i]) for i in indices]
        
        # Need a normal to the plane so that we can know which vertices form inner angles
        normal = findOuterNormal(face)
            
        if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
            return
        
        # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
        n = len(face)
        vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
        while n > 3:
            max_cos = EPSILON # We don't want to check anything on Pi angles
            i_min = 0 # max cos corresponds to min angle
            for i in range(n):
                inext = (i + 1) % n
                iprev = (i + n - 1) % n
                v = face[vi[i]]
                next = face[vi[inext]] - v
                prev = face[vi[iprev]] - v
                nextXprev = next.cross(prev)
                if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
                    cos = next.dot(prev) / (next.length() * prev.length())
                    if cos > max_cos:
                        # Check if there are vertices inside the triangle
                        no_points_inside = True
                        for j in range(n):
                            if j != i and j != iprev and j != inext:
                                vx = face[vi[j]] - v
                                if pointInsideTriangle(vx, next, prev, nextXprev):
                                    no_points_inside = False
                                    break
                                
                        if no_points_inside:
                            max_cos = cos
                            i_min = i
                            
            self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
            vi.pop(i_min)
            n -= 1
        self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
예제 #22
0
class Camera(SceneNode.SceneNode):
    def __init__(self, name, parent=None):
        super().__init__(parent)
        self._name = name
        self._projection_matrix = Matrix()
        self._projection_matrix.setOrtho(-5, 5, 5, -5, -100, 100)
        self._perspective = False
        self._viewport_width = 0
        self._viewport_height = 0
        self._window_width = 0
        self._window_height = 0

        self.setCalculateBoundingBox(False)

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self):
        return copy.deepcopy(self._projection_matrix)

    def getViewportWidth(self):
        return self._viewport_width

    def setViewportWidth(self, width):
        self._viewport_width = width

    def setViewPortHeight(self, height):
        self._viewport_height = height

    def setViewportSize(self, width, height):
        self._viewport_width = width
        self._viewport_height = height

    def getViewportHeight(self):
        return self._viewport_height

    def setWindowSize(self, w, h):
        self._window_width = w
        self._window_height = h

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

    def isPerspective(self):
        return self._perspective

    def setPerspective(self, pers):
        self._perspective = pers

    ##  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, y):
        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

        invp = numpy.linalg.inv(self._projection_matrix.getData().copy())
        invv = self.getWorldTransformation().getData()

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

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

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

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

    ##  Project a 3D position onto the 2D view plane.
    def project(self, position):
        projection = self._projection_matrix
        view = self.getWorldTransformation().getInverse()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)

        return (position.x / position.z / 2.0, position.y / position.z / 2.0)
예제 #23
0
    def addDonut(self, **kwargs):
        inner_radius = kwargs["inner_radius"]
        outer_radius = kwargs["outer_radius"]
        width = kwargs["width"]

        center = kwargs.get("center", Vector(0, 0, 0))
        sections = kwargs.get("sections", 32)
        color = kwargs.get("color", None)

        angle = kwargs.get("angle", 0)
        axis = kwargs.get("axis", Vector.Unit_Y)

        vertices = []
        indices = []
        colors = []

        start = self._mesh_data.getVertexCount()

        for i in range(sections):
            v1 = start + i * 3
            v2 = v1 + 1
            v3 = v1 + 2
            v4 = v1 + 3
            v5 = v1 + 4
            v6 = v1 + 5

            if i+1 >= sections: # connect the end to the start
                v4 = start
                v5 = start + 1
                v6 = start + 2

            theta = i * math.pi / (sections / 2)
            c = math.cos(theta)
            s = math.sin(theta)

            vertices.append( [inner_radius * c, inner_radius * s, 0] )
            vertices.append( [outer_radius * c, outer_radius * s, width] )
            vertices.append( [outer_radius * c, outer_radius * s, -width] )

            indices.append( [v1, v4, v5] )
            indices.append( [v2, v1, v5] )

            indices.append( [v2, v5, v6] )
            indices.append( [v3, v2, v6] )

            indices.append( [v3, v6, v4] )
            indices.append( [v1, v3, v4] )

            if color:
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )
                colors.append( [color.r, color.g, color.b, color.a] )

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        vertices = numpy.asarray(vertices, dtype = numpy.float32)
        vertices = vertices.dot(matrix.getData()[0:3,0:3])
        vertices[:] += center.getData()
        self._mesh_data.addVertices(vertices)

        self._mesh_data.addIndices(numpy.asarray(indices, dtype = numpy.int32))
        self._mesh_data.addColors(numpy.asarray(colors, dtype = numpy.float32))
예제 #24
0
class X3DReader(MeshReader):
    def __init__(self) -> None:
        super().__init__()
        self._supported_extensions = [".x3d"]
        self._namespaces = {}

    # Main entry point
    # Reads the file, returns a SceneNode (possibly with nested ones), or None
    def _read(self, file_name):
        try:
            self.defs = {}
            self.shapes = []

            tree = ET.parse(file_name)
            xml_root = tree.getroot()

            if xml_root.tag != "X3D":
                return None

            scale = 1000  # Default X3D unit it one meter, while Cura's is one millimeters
            if xml_root[0].tag == "head":
                for head_node in xml_root[0]:
                    if head_node.tag == "unit" and head_node.attrib.get(
                            "category") == "length":
                        scale *= float(head_node.attrib["conversionFactor"])
                        break
                xml_scene = xml_root[1]
            else:
                xml_scene = xml_root[0]

            if xml_scene.tag != "Scene":
                return None

            self.transform = Matrix()
            self.transform.setByScaleFactor(scale)
            self.index_base = 0

            # Traverse the scene tree, populate the shapes list
            self.processChildNodes(xml_scene)

            if self.shapes:
                builder = MeshBuilder()
                builder.setVertices(
                    numpy.concatenate([shape.verts for shape in self.shapes]))
                builder.setIndices(
                    numpy.concatenate([shape.faces for shape in self.shapes]))
                builder.calculateNormals()
                builder.setFileName(file_name)
                mesh_data = builder.build()

                # Manually try and get the extents of the mesh_data. This should prevent nasty NaN issues from
                # leaving the reader.
                mesh_data.getExtents()

                node = SceneNode()
                node.setMeshData(mesh_data)
                node.setSelectable(True)
                node.setName(file_name)

            else:
                return None

        except Exception:
            Logger.logException("e", "Exception in X3D reader")
            return None

        return node

    # ------------------------- XML tree traversal

    def processNode(self, xml_node):
        xml_node = self.resolveDefUse(xml_node)
        if xml_node is None:
            return

        tag = xml_node.tag
        if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace",
                   "CADLayer", "Collision"):
            self.processChildNodes(xml_node)
        if tag == "CADPart":
            self.processTransform(xml_node)  # TODO: split the parts
        elif tag == "LOD":
            self.processNode(xml_node[0])
        elif tag == "Transform":
            self.processTransform(xml_node)
        elif tag == "Shape":
            self.processShape(xml_node)

    def processShape(self, xml_node):
        # Find the geometry and the appearance inside the Shape
        geometry = appearance = None
        for sub_node in xml_node:
            if sub_node.tag == "Appearance" and not appearance:
                appearance = self.resolveDefUse(sub_node)
            elif sub_node.tag in self.geometry_importers and not geometry:
                geometry = self.resolveDefUse(sub_node)

        # TODO: appearance is completely ignored. At least apply the material color...
        if not geometry is None:
            try:
                self.verts = self.faces = []  # Safeguard
                self.geometry_importers[geometry.tag](self, geometry)
                m = self.transform.getData()
                verts = m.dot(self.verts)[:3].transpose()

                self.shapes.append(
                    Shape(verts, self.faces, self.index_base, geometry.tag))
                self.index_base += len(verts)

            except Exception:
                Logger.logException(
                    "e", "Exception in X3D reader while reading %s",
                    geometry.tag)

    # Returns the referenced node if the node has USE, the same node otherwise.
    # May return None is USE points at a nonexistent node
    # In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
    # Big caveat: XML element objects may evaluate to boolean False!!!
    # Don't ever use "if node:", use "if not node is None:" instead
    def resolveDefUse(self, node):
        USE = node.attrib.get("USE")
        if USE:
            return self.defs.get(USE, None)

        DEF = node.attrib.get("DEF")
        if DEF:
            self.defs[DEF] = node
        return node

    def processChildNodes(self, node):
        for c in node:
            self.processNode(c)
            Job.yieldThread()

    # Since this is a grouping node, will recurse down the tree.
    # According to the spec, the final transform matrix is:
    # T * C * R * SR * S * -SR * -C
    # Where SR corresponds to the rotation matrix to scaleOrientation
    # C and SR are rather exotic. S, slightly less so.
    def processTransform(self, node):
        rot = readRotation(node, "rotation",
                           (0, 0, 1, 0))  # (angle, axisVactor) tuple
        trans = readVector(node, "translation", (0, 0, 0))  # Vector
        scale = readVector(node, "scale", (1, 1, 1))  # Vector
        center = readVector(node, "center", (0, 0, 0))  # Vector
        scale_orient = readRotation(node, "scaleOrientation",
                                    (0, 0, 1, 0))  # (angle, axisVactor) tuple

        # Store the previous transform; in Cura, the default matrix multiplication is in place
        prev = Matrix(self.transform.getData())  # It's deep copy, I've checked

        # The rest of transform manipulation will be applied in place
        got_center = (center.x != 0 or center.y != 0 or center.z != 0)

        T = self.transform
        if trans.x != 0 or trans.y != 0 or trans.z != 0:
            T.translate(trans)
        if got_center:
            T.translate(center)
        if rot[0] != 0:
            T.rotateByAxis(*rot)
        if scale.x != 1 or scale.y != 1 or scale.z != 1:
            got_scale_orient = scale_orient[0] != 0
            if got_scale_orient:
                T.rotateByAxis(*scale_orient)
            # No scale by vector in place operation in UM
            S = Matrix()
            S.setByScaleVector(scale)
            T.multiply(S)
            if got_scale_orient:
                T.rotateByAxis(-scale_orient[0], scale_orient[1])
        if got_center:
            T.translate(-center)

        self.processChildNodes(node)
        self.transform = prev

    # ------------------------- Geometry importers
    # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest

    # Primitives

    def processGeometryBox(self, node):
        (dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
        dx /= 2
        dy /= 2
        dz /= 2
        self.reserveFaceAndVertexCount(12, 8)

        # xz plane at +y, ccw
        self.addVertex(dx, dy, dz)
        self.addVertex(-dx, dy, dz)
        self.addVertex(-dx, dy, -dz)
        self.addVertex(dx, dy, -dz)
        # xz plane at -y
        self.addVertex(dx, -dy, dz)
        self.addVertex(-dx, -dy, dz)
        self.addVertex(-dx, -dy, -dz)
        self.addVertex(dx, -dy, -dz)

        self.addQuad(0, 1, 2, 3)  # +y
        self.addQuad(4, 0, 3, 7)  # +x
        self.addQuad(7, 3, 2, 6)  # -z
        self.addQuad(6, 2, 1, 5)  # -x
        self.addQuad(5, 1, 0, 4)  # +z
        self.addQuad(7, 6, 5, 4)  # -y

    # The sphere is subdivided into nr rings and ns segments
    def processGeometrySphere(self, node):
        r = readFloat(node, "radius", 0.5)
        subdiv = readIntArray(node, "subdivision", None)
        if subdiv:
            if len(subdiv) == 1:
                nr = ns = subdiv[0]
            else:
                (nr, ns) = subdiv
        else:
            nr = ns = DEFAULT_SUBDIV

        lau = pi / nr  # Unit angle of latitude (rings) for the given tesselation
        lou = 2 * pi / ns  # Unit angle of longitude (segments)

        self.reserveFaceAndVertexCount(ns * (nr * 2 - 2), 2 + (nr - 1) * ns)

        # +y and -y poles
        self.addVertex(0, r, 0)
        self.addVertex(0, -r, 0)

        # The non-polar vertices go from x=0, negative z plane counterclockwise -
        # to -x, to +z, to +x, back to -z
        for ring in range(1, nr):
            for seg in range(ns):
                self.addVertex(-r * sin(lou * seg) * sin(lau * ring),
                               r * cos(lau * ring),
                               -r * cos(lou * seg) * sin(lau * ring))

        vb = 2 + (nr - 2) * ns  # First vertex index for the bottom cap

        # Faces go in order: top cap, sides, bottom cap.
        # Sides go by ring then by segment.

        # Caps
        # Top cap face vertices go in order: down right up
        # (starting from +y pole)
        # Bottom cap goes: up left down (starting from -y pole)
        for seg in range(ns):
            self.addTri(0, seg + 2, (seg + 1) % ns + 2)
            self.addTri(1, vb + (seg + 1) % ns, vb + seg)

        # Sides
        # Side face vertices go in order:  down right upleft, downright up left
        for ring in range(nr - 2):
            tvb = 2 + ring * ns
            # First vertex index for the top edge of the ring
            bvb = tvb + ns
            # First vertex index for the bottom edge of the ring
            for seg in range(ns):
                nseg = (seg + 1) % ns
                self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)

    def processGeometryCone(self, node):
        r = readFloat(node, "bottomRadius", 1)
        height = readFloat(node, "height", 2)
        bottom = readBoolean(node, "bottom", True)
        side = readBoolean(node, "side", True)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)

        d = height / 2
        angle = 2 * pi / n

        self.reserveFaceAndVertexCount(
            (n if side else 0) + (n - 2 if bottom else 0), n + 1)

        # Vertex 0 is the apex, vertices 1..n are the bottom
        self.addVertex(0, d, 0)
        for i in range(n):
            self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))

        # Side face vertices go: up down right
        if side:
            for i in range(n):
                self.addTri(1 + (i + 1) % n, 0, 1 + i)
        if bottom:
            for i in range(2, n):
                self.addTri(1, i, i + 1)

    def processGeometryCylinder(self, node):
        r = readFloat(node, "radius", 1)
        height = readFloat(node, "height", 2)
        bottom = readBoolean(node, "bottom", True)
        side = readBoolean(node, "side", True)
        top = readBoolean(node, "top", True)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)

        nn = n * 2
        angle = 2 * pi / n
        hh = height / 2

        self.reserveFaceAndVertexCount(
            (nn if side else 0) + (n - 2 if top else 0) +
            (n - 2 if bottom else 0), nn)

        # The seam is at x=0, z=-r, vertices go ccw -
        # to pos x, to neg z, to neg x, back to neg z
        for i in range(n):
            rs = -r * sin(angle * i)
            rc = -r * cos(angle * i)
            self.addVertex(rs, hh, rc)
            self.addVertex(rs, -hh, rc)

        if side:
            for i in range(n):
                ni = (i + 1) % n
                self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)

        for i in range(2, nn - 3, 2):
            if top:
                self.addTri(0, i, i + 2)
            if bottom:
                self.addTri(1, i + 1, i + 3)

    # Semi-primitives

    def processGeometryElevationGrid(self, node):
        dx = readFloat(node, "xSpacing", 1)
        dz = readFloat(node, "zSpacing", 1)
        nx = readInt(node, "xDimension", 0)
        nz = readInt(node, "zDimension", 0)
        height = readFloatArray(node, "height", False)
        ccw = readBoolean(node, "ccw", True)

        if nx <= 0 or nz <= 0 or len(height) < nx * nz:
            return  # That's weird, the wording of the standard suggests grids with zero quads are somehow valid

        self.reserveFaceAndVertexCount(2 * (nx - 1) * (nz - 1), nx * nz)

        for z in range(nz):
            for x in range(nx):
                self.addVertex(x * dx, height[z * nx + x], z * dz)

        for z in range(1, nz):
            for x in range(1, nx):
                self.addTriFlip((z - 1) * nx + x - 1, z * nx + x,
                                (z - 1) * nx + x, ccw)
                self.addTriFlip((z - 1) * nx + x - 1, z * nx + x - 1,
                                z * nx + x, ccw)

    def processGeometryExtrusion(self, node):
        ccw = readBoolean(node, "ccw", True)
        begin_cap = readBoolean(node, "beginCap", True)
        end_cap = readBoolean(node, "endCap", True)
        cross = readFloatArray(node, "crossSection",
                               (1, 1, 1, -1, -1, -1, -1, 1, 1, 1))
        cross = [(cross[i], cross[i + 1]) for i in range(0, len(cross), 2)]
        spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0))
        spine = [(spine[i], spine[i + 1], spine[i + 2])
                 for i in range(0, len(spine), 3)]
        orient = readFloatArray(node, "orientation", None)
        if orient:
            # This converts X3D's axis/angle rotation to a 3x3 numpy matrix
            def toRotationMatrix(rot):
                (x, y, z) = rot[:3]
                a = rot[3]
                s = sin(a)
                c = cos(a)
                t = 1 - c
                return numpy.array(
                    ((x * x * t + c, x * y * t - z * s, x * z * t + y * s),
                     (x * y * t + z * s, y * y * t + c, y * z * t - x * s),
                     (x * z * t - y * s, y * z * t + x * s, z * z * t + c)))

            orient = [
                toRotationMatrix(orient[i:i +
                                        4]) if orient[i + 3] != 0 else None
                for i in range(0, len(orient), 4)
            ]

        scale = readFloatArray(node, "scale", None)
        if scale:
            scale = [
                numpy.array(
                    ((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i + 1])))
                if scale[i] != 1 or scale[i + 1] != 1 else None
                for i in range(0, len(scale), 2)
            ]

        # Special treatment for the closed spine and cross section.
        # Let's save some memory by not creating identical but distinct vertices;
        # later we'll introduce conditional logic to link the last vertex with
        # the first one where necessary.
        crossClosed = cross[0] == cross[-1]
        if crossClosed:
            cross = cross[:-1]
        nc = len(cross)
        cross = [numpy.array((c[0], 0, c[1])) for c in cross]
        ncf = nc if crossClosed else nc - 1
        # Face count along the cross; for closed cross, it's the same as the
        # respective vertex count

        spine_closed = spine[0] == spine[-1]
        if spine_closed:
            spine = spine[:-1]
        ns = len(spine)
        spine = [Vector(*s) for s in spine]
        nsf = ns if spine_closed else ns - 1

        # This will be used for fallback, where the current spine point joins
        # two collinear spine segments. No need to recheck the case of the
        # closed spine/last-to-first point juncture; if there's an angle there,
        # it would kick in on the first iteration of the main loop by spine.
        def findFirstAngleNormal():
            for i in range(1, ns - 1):
                spt = spine[i]
                z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
                if z.length() > EPSILON:
                    return z
            # All the spines are collinear. Fallback to the rotated source
            # XZ plane.
            # TODO: handle the situation where the first two spine points match
            if len(spine) < 2:
                return Vector(0, 0, 1)
            v = spine[1] - spine[0]
            orig_y = Vector(0, 1, 0)
            orig_z = Vector(0, 0, 1)
            if v.cross(orig_y).length() > EPSILON:
                # Spine at angle with global y - rotate the z accordingly
                a = v.cross(orig_y)  # Axis of rotation to get to the Z
                (x, y, z) = a.normalized().getData()
                s = a.length() / v.length()
                c = sqrt(1 - s * s)
                t = 1 - c
                m = numpy.array(
                    ((x * x * t + c, x * y * t + z * s, x * z * t - y * s),
                     (x * y * t - z * s, y * y * t + c, y * z * t + x * s),
                     (x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
                orig_z = Vector(*m.dot(orig_z.getData()))
            return orig_z

        self.reserveFaceAndVertexCount(
            2 * nsf * ncf + (nc - 2 if begin_cap else 0) +
            (nc - 2 if end_cap else 0), ns * nc)

        z = None
        for i, spt in enumerate(spine):
            if (i > 0 and i < ns - 1) or spine_closed:
                snext = spine[(i + 1) % ns]
                sprev = spine[(i - 1 + ns) % ns]
                y = snext - sprev
                vnext = snext - spt
                vprev = sprev - spt
                try_z = vnext.cross(vprev)
                # Might be zero, then all kinds of fallback
                if try_z.length() > EPSILON:
                    if z is not None and try_z.dot(z) < 0:
                        try_z = -try_z
                    z = try_z
                elif not z:  # No z, and no previous z.
                    # Look ahead, see if there's at least one point where
                    # spines are not collinear.
                    z = findFirstAngleNormal()
            elif i == 0:  # And non-crossed
                snext = spine[i + 1]
                y = snext - spt
                z = findFirstAngleNormal()
            else:  # last point and not crossed
                sprev = spine[i - 1]
                y = spt - sprev
                # If there's more than one point in the spine, z is already set.
                # One point in the spline is an error anyway.

            z = z.normalized()
            y = y.normalized()
            x = y.cross(z)  # Already normalized
            m = numpy.array(
                ((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))

            # Columns are the unit vectors for the xz plane for the cross-section
            if orient:
                mrot = orient[i] if len(orient) > 1 else orient[0]
                if not mrot is None:
                    m = m.dot(
                        mrot
                    )  # Tested against X3DOM, the result matches, still not sure :(

            if scale:
                mscale = scale[i] if len(scale) > 1 else scale[0]
                if not mscale is None:
                    m = m.dot(mscale)

            # First the cross-section 2-vector is scaled,
            # then rotated (which may make it a 3-vector),
            # then applied to the xz plane unit vectors

            sptv3 = numpy.array(spt.getData()[:3])
            for cpt in cross:
                v = sptv3 + m.dot(cpt)
                self.addVertex(*v)

        if begin_cap:
            self.addFace([x for x in range(nc - 1, -1, -1)], ccw)

        # Order of edges in the face: forward along cross, forward along spine,
        # backward along cross, backward along spine, flipped if now ccw.
        # This order is assumed later in the texture coordinate assignment;
        # please don't change without syncing.

        for s in range(ns - 1):
            for c in range(ncf):
                self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
                                 (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c,
                                 ccw)

        if spine_closed:
            # The faces between the last and the first spine points
            b = (ns - 1) * nc
            for c in range(ncf):
                self.addQuadFlip(b + c, b + (c + 1) % nc, (c + 1) % nc, c, ccw)

        if end_cap:
            self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)

# Triangle meshes

# Helper for numerous nodes with a Coordinate subnode holding vertices
# That all triangle meshes and IndexedFaceSet
# num_faces can be a function, in case the face count is a function of vertex count

    def startCoordMesh(self, node, num_faces):
        ccw = readBoolean(node, "ccw", True)
        self.readVertices(node)  # This will allocate and fill the vertex array
        if hasattr(num_faces, "__call__"):
            num_faces = num_faces(self.getVertexCount())
        self.reserveFaceCount(num_faces)

        return ccw

    def processGeometryIndexedTriangleSet(self, node):
        index = readIntArray(node, "index", [])
        num_faces = len(index) // 3
        ccw = int(self.startCoordMesh(node, num_faces))

        for i in range(0, num_faces * 3, 3):
            self.addTri(index[i + 1 - ccw], index[i + ccw], index[i + 2])

    def processGeometryIndexedTriangleStripSet(self, node):
        strips = readIndex(node, "index")
        ccw = int(
            self.startCoordMesh(node,
                                sum([len(strip) - 2 for strip in strips])))

        for strip in strips:
            sccw = ccw  # Running CCW value, reset for each strip
            for i in range(len(strip) - 2):
                self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i + 2])
                sccw = 1 - sccw

    def processGeometryIndexedTriangleFanSet(self, node):
        fans = readIndex(node, "index")
        ccw = int(
            self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))

        for fan in fans:
            for i in range(1, len(fan) - 1):
                self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])

    def processGeometryTriangleSet(self, node):
        ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
        for i in range(0, self.getVertexCount(), 3):
            self.addTri(i + 1 - ccw, i + ccw, i + 2)

    def processGeometryTriangleStripSet(self, node):
        strips = readIntArray(node, "stripCount", [])
        ccw = int(self.startCoordMesh(node, sum([n - 2 for n in strips])))

        vb = 0
        for n in strips:
            sccw = ccw
            for i in range(n - 2):
                self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
                sccw = 1 - sccw
            vb += n

    def processGeometryTriangleFanSet(self, node):
        fans = readIntArray(node, "fanCount", [])
        ccw = int(self.startCoordMesh(node, sum([n - 2 for n in fans])))

        vb = 0
        for n in fans:
            for i in range(1, n - 1):
                self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
            vb += n

    # Quad geometries from the CAD module, might be relevant for printing

    def processGeometryQuadSet(self, node):
        ccw = self.startCoordMesh(node, lambda num_vert: 2 * (num_vert // 4))
        for i in range(0, self.getVertexCount(), 4):
            self.addQuadFlip(i, i + 1, i + 2, i + 3, ccw)

    def processGeometryIndexedQuadSet(self, node):
        index = readIntArray(node, "index", [])
        num_quads = len(index) // 4
        ccw = self.startCoordMesh(node, num_quads * 2)

        for i in range(0, num_quads * 4, 4):
            self.addQuadFlip(index[i], index[i + 1], index[i + 2],
                             index[i + 3], ccw)

    # 2D polygon geometries
    # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
    # The only way around that is merging meshes.

    def processGeometryDisk2D(self, node):
        innerRadius = readFloat(node, "innerRadius", 0)
        outerRadius = readFloat(node, "outerRadius", 1)
        n = readInt(node, "subdivision", DEFAULT_SUBDIV)

        angle = 2 * pi / n

        self.reserveFaceAndVertexCount(n * 4 if innerRadius else n - 2,
                                       n * 2 if innerRadius else n)

        for i in range(n):
            s = sin(angle * i)
            c = cos(angle * i)
            self.addVertex(outerRadius * c, outerRadius * s, 0)
            if innerRadius:
                self.addVertex(innerRadius * c, innerRadius * s, 0)
                ni = (i + 1) % n
                self.addQuad(2 * i, 2 * ni, 2 * ni + 1, 2 * i + 1)

        if not innerRadius:
            for i in range(2, n):
                self.addTri(0, i - 1, i)

    def processGeometryRectangle2D(self, node):
        (x, y) = readFloatArray(node, "size", (2, 2))
        self.reserveFaceAndVertexCount(2, 4)
        self.addVertex(-x / 2, -y / 2, 0)
        self.addVertex(x / 2, -y / 2, 0)
        self.addVertex(x / 2, y / 2, 0)
        self.addVertex(-x / 2, y / 2, 0)
        self.addQuad(0, 1, 2, 3)

    def processGeometryTriangleSet2D(self, node):
        verts = readFloatArray(node, "vertices", ())
        num_faces = len(verts) // 6
        verts = [(verts[i], verts[i + 1], 0)
                 for i in range(0, 6 * num_faces, 2)]
        self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
        for vert in verts:
            self.addVertex(*vert)

        # The front face is on the +Z side, so CCW is a variable
        for i in range(0, num_faces * 3, 3):
            a = Vector(*verts[i + 2]) - Vector(*verts[i])
            b = Vector(*verts[i + 1]) - Vector(*verts[i])
            self.addTriFlip(i, i + 1, i + 2, a.x * b.y > a.y * b.x)

    # General purpose polygon mesh

    def processGeometryIndexedFaceSet(self, node):
        faces = readIndex(node, "coordIndex")
        ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))

        for face in faces:
            if len(face) == 3:
                self.addTriFlip(face[0], face[1], face[2], ccw)
            elif len(face) > 3:
                self.addFace(face, ccw)

    geometry_importers = {
        "IndexedFaceSet": processGeometryIndexedFaceSet,
        "IndexedTriangleSet": processGeometryIndexedTriangleSet,
        "IndexedTriangleStripSet": processGeometryIndexedTriangleStripSet,
        "IndexedTriangleFanSet": processGeometryIndexedTriangleFanSet,
        "TriangleSet": processGeometryTriangleSet,
        "TriangleStripSet": processGeometryTriangleStripSet,
        "TriangleFanSet": processGeometryTriangleFanSet,
        "QuadSet": processGeometryQuadSet,
        "IndexedQuadSet": processGeometryIndexedQuadSet,
        "TriangleSet2D": processGeometryTriangleSet2D,
        "Rectangle2D": processGeometryRectangle2D,
        "Disk2D": processGeometryDisk2D,
        "ElevationGrid": processGeometryElevationGrid,
        "Extrusion": processGeometryExtrusion,
        "Sphere": processGeometrySphere,
        "Box": processGeometryBox,
        "Cylinder": processGeometryCylinder,
        "Cone": processGeometryCone
    }

    # Parses the Coordinate.@point field, fills the verts array.
    def readVertices(self, node):
        for c in node:
            if c.tag == "Coordinate":
                c = self.resolveDefUse(c)
                if not c is None:
                    pt = c.attrib.get("point")
                    if pt:
                        # allow the list of float values in 'point' attribute to
                        # be separated by commas or whitespace as per spec of
                        # XML encoding of X3D
                        # Ref  ISO/IEC 19776-1:2015 : Section 5.1.2
                        co = [
                            float(x) for vec in pt.split(',')
                            for x in vec.split()
                        ]
                        num_verts = len(co) // 3
                        self.verts = numpy.empty((4, num_verts),
                                                 dtype=numpy.float32)
                        self.verts[3, :] = numpy.ones((num_verts),
                                                      dtype=numpy.float32)
                        # Group by three
                        for i in range(num_verts):
                            self.verts[:3, i] = co[3 * i:3 * i + 3]

    # Mesh builder helpers

    def reserveFaceAndVertexCount(self, num_faces, num_verts):
        # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
        self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
        self.verts[3, :] = numpy.ones((num_verts), dtype=numpy.float32)
        self.num_verts = 0
        self.reserveFaceCount(num_faces)

    def reserveFaceCount(self, num_faces):
        self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
        self.num_faces = 0

    def getVertexCount(self):
        return self.verts.shape[1]

    def addVertex(self, x, y, z):
        self.verts[0, self.num_verts] = x
        self.verts[1, self.num_verts] = y
        self.verts[2, self.num_verts] = z
        self.num_verts += 1

    # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
    def addTri(self, a, b, c):
        self.faces[self.num_faces, 0] = self.index_base + a
        self.faces[self.num_faces, 1] = self.index_base + b
        self.faces[self.num_faces, 2] = self.index_base + c
        self.num_faces += 1

    def addTriFlip(self, a, b, c, ccw):
        if ccw:
            self.addTri(a, b, c)
        else:
            self.addTri(b, a, c)

    # Needs to be convex, but not necessaily planar
    # Assumed ccw, cut along the ac diagonal
    def addQuad(self, a, b, c, d):
        self.addTri(a, b, c)
        self.addTri(c, d, a)

    def addQuadFlip(self, a, b, c, d, ccw):
        if ccw:
            self.addTri(a, b, c)
            self.addTri(c, d, a)
        else:
            self.addTri(a, c, b)
            self.addTri(c, a, d)

    # Arbitrary polygon triangulation.
    # Doesn't assume convexity and doesn't check the "convex" flag in the file.
    # Works by the "cutting of ears" algorithm:
    # - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
    # - Remove the triangle at that vertex
    # - Repeat until done
    # Vertex coordinates are supposed to be already set
    def addFace(self, indices, ccw):
        # Resolve indices to coordinates for faster math
        face = [Vector(data=self.verts[0:3, i]) for i in indices]

        # Need a normal to the plane so that we can know which vertices form inner angles
        normal = findOuterNormal(face)

        if not normal:  # Couldn't find an outer edge, non-planar polygon maybe?
            return

        # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
        n = len(face)
        vi = [i for i in range(n)
              ]  # We'll be using this to kick vertices from the face
        while n > 3:
            max_cos = EPSILON  # We don't want to check anything on Pi angles
            i_min = 0  # max cos corresponds to min angle
            for i in range(n):
                inext = (i + 1) % n
                iprev = (i + n - 1) % n
                v = face[vi[i]]
                next = face[vi[inext]] - v
                prev = face[vi[iprev]] - v
                nextXprev = next.cross(prev)
                if nextXprev.dot(normal) > EPSILON:  # If it's an inner angle
                    cos = next.dot(prev) / (next.length() * prev.length())
                    if cos > max_cos:
                        # Check if there are vertices inside the triangle
                        no_points_inside = True
                        for j in range(n):
                            if j != i and j != iprev and j != inext:
                                vx = face[vi[j]] - v
                                if pointInsideTriangle(vx, next, prev,
                                                       nextXprev):
                                    no_points_inside = False
                                    break

                        if no_points_inside:
                            max_cos = cos
                            i_min = i

            self.addTriFlip(indices[vi[(i_min + n - 1) % n]],
                            indices[vi[i_min]], indices[vi[(i_min + 1) % n]],
                            ccw)
            vi.pop(i_min)
            n -= 1
        self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
예제 #25
0
    def test_setByScaleFactor(self):
        matrix = Matrix()
        matrix.setByScaleFactor(0.5)
        numpy.testing.assert_array_almost_equal(matrix.getData(), numpy.array([[0.5,0,0,0],[0,0.5,0,0],[0,0,0.5,0],[0,0,0,1]]))

        assert matrix.getScale() == Vector(0.5, 0.5, 0.5)
예제 #26
0
class Camera(SceneNode.SceneNode):
    def __init__(self, name, parent = None):
        super().__init__(parent)
        self._name = name
        self._projection_matrix = Matrix()
        self._projection_matrix.setOrtho(-5, 5, 5, -5, -100, 100)
        self._perspective = False
        self._viewport_width = 0
        self._viewport_height = 0
        self._window_width = 0
        self._window_height = 0

        self.setCalculateBoundingBox(False)

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self):
        return copy.deepcopy(self._projection_matrix)
    
    def getViewportWidth(self):
        return self._viewport_width
    
    def setViewportWidth(self, width):
        self._viewport_width = width
    
    def setViewPortHeight(self,height):
        self._viewport_height = height
        
    def setViewportSize(self,width,height):
        self._viewport_width = width
        self._viewport_height = height
    
    def getViewportHeight(self):
        return self._viewport_height

    def setWindowSize(self, w, h):
        self._window_width = w
        self._window_height = h
    
    ##  Set the projection matrix of this camera.
    #   \param matrix The projection matrix to use for this camera.
    def setProjectionMatrix(self, matrix):
        self._projection_matrix = matrix

    def isPerspective(self):
        return self._perspective

    def setPerspective(self, pers):
        self._perspective = pers

    ##  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, y):
        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]

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

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

    ##  Project a 3D position onto the 2D view plane.
    def project(self, position):
        projection = self._projection_matrix
        view = self.getWorldTransformation().getInverse()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)

        return position.x / position.z / 2.0, position.y / position.z / 2.0
예제 #27
0
    def addDonut(self,
                 inner_radius,
                 outer_radius,
                 width,
                 center=Vector(0, 0, 0),
                 sections=32,
                 color=None,
                 angle=0,
                 axis=Vector.Unit_Y):
        """Adds a torus to the mesh of this mesh builder.

        The torus is the shape of a doughnut. This doughnut is delicious and
        moist, but not very healthy.

        :param inner_radius: The radius of the hole inside the torus. Must be
        smaller than outer_radius.
        :param outer_radius: The radius of the outside of the torus. Must be
        larger than inner_radius.
        :param width: The radius of the torus in perpendicular direction to its
        perimeter. This is the "thickness".
        :param center: (Optional) The position of the centre of the torus. If no
        position is provided, the torus will be centred around the coordinate
        origin.
        :param sections: (Optional) The resolution of the torus in the
        circumference. The resolution of the intersection of the torus cannot be
        changed.
        :param color: (Optional) The colour of the torus. If no colour is
        provided, a colour will be determined by the shader.
        :param angle: (Optional) An angle of rotation to rotate the torus by, in
        radians.
        :param axis: (Optional) An axis of rotation to rotate the torus around.
        If no axis is provided and the angle of rotation is nonzero, the torus
        will be rotated around the Y-axis.
        """

        vertices = []
        indices = []
        colors = []

        start = self.getVertexCount()  #Starting index.

        for i in range(sections):
            v1 = start + i * 3  #Indices for each of the vertices we'll add for this section.
            v2 = v1 + 1
            v3 = v1 + 2
            v4 = v1 + 3
            v5 = v1 + 4
            v6 = v1 + 5

            if i + 1 >= sections:  # connect the end to the start
                v4 = start
                v5 = start + 1
                v6 = start + 2

            theta = i * math.pi / (
                sections / 2)  #Angle of this piece around torus perimeter.
            c = math.cos(theta)  #X-coordinate around torus perimeter.
            s = math.sin(theta)  #Y-coordinate around torus perimeter.

            #One vertex on the inside perimeter, two on the outside perimiter (up and down).
            vertices.append([inner_radius * c, inner_radius * s, 0])
            vertices.append([outer_radius * c, outer_radius * s, width])
            vertices.append([outer_radius * c, outer_radius * s, -width])

            #Connect the vertices to the next segment.
            indices.append([v1, v4, v5])
            indices.append([v2, v1, v5])

            indices.append([v2, v5, v6])
            indices.append([v3, v2, v6])

            indices.append([v3, v6, v4])
            indices.append([v1, v3, v4])

            if color:  #If we have a colour, add it to the vertices.
                colors.append([color.r, color.g, color.b, color.a])
                colors.append([color.r, color.g, color.b, color.a])
                colors.append([color.r, color.g, color.b, color.a])

        #Rotate the resulting torus around the specified axis.
        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        vertices = numpy.asarray(vertices, dtype=numpy.float32)
        vertices = vertices.dot(matrix.getData()[0:3, 0:3])
        vertices[:] += center.getData(
        )  #And translate to the desired position.

        self.addVertices(vertices)
        self.addIndices(numpy.asarray(indices, dtype=numpy.int32))
        self.addColors(numpy.asarray(colors, dtype=numpy.float32))
예제 #28
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
예제 #29
0
파일: Camera.py 프로젝트: derekhe/Uranium
class Camera(SceneNode.SceneNode):
    def __init__(self, name, parent = None):
        super().__init__(parent)
        self._name = name
        self._projection_matrix = Matrix()
        self._projection_matrix.setOrtho(-5, 5, 5, -5, -100, 100)
        self._perspective = False
        self._viewport_width = 0
        self._viewport_height = 0

        self.setCalculateBoundingBox(False)

    ##  Get the projection matrix of this camera.
    def getProjectionMatrix(self):
        return self._projection_matrix
    
    def getViewportWidth(self):
        return self._viewport_width
    
    def setViewportWidth(self, width):
        self._viewport_width = width
    
    def setViewPortHeight(self,height):
        self._viewport_height = height
        
    def setViewportSize(self,width,height):
        self._viewport_width = width
        self._viewport_height = height
    
    def getViewportHeight(self):
        return self._viewport_height
    
    ##  Set the projection matrix of this camera.
    #   \param matrix The projection matrix to use for this camera.
    def setProjectionMatrix(self, matrix):
        self._projection_matrix = matrix

    def isPerspective(self):
        return self._perspective

    def setPerspective(self, pers):
        self._perspective = pers

    ##  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, y):
        invp = numpy.linalg.inv(self._projection_matrix.getData().copy())
        invv = self.getWorldTransformation().getData()

        near = numpy.array([x, -y, -1.0, 1.0], dtype=numpy.float32)
        near = numpy.dot(invp, near)
        near = numpy.dot(invv, near)
        near = near[0:3] / near[3]

        far = numpy.array([x, -y, 1.0, 1.0], dtype = numpy.float32)
        far = numpy.dot(invp, far)
        far = numpy.dot(invv, far)
        far = far[0:3] / far[3]

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

        return Ray(self.getWorldPosition(), Vector(-dir[0], -dir[1], -dir[2]))
예제 #30
0
    def addPyramid(self,
                   width,
                   height,
                   depth,
                   angle=0,
                   axis=Vector.Unit_Y,
                   center=Vector(0, 0, 0),
                   color=None):
        """Adds a pyramid to the mesh of this mesh builder.

        :param width: The width of the base of the pyramid.
        :param height: The height of the pyramid (from base to notch).
        :param depth: The depth of the base of the pyramid.
        :param angle: (Optional) An angle of rotation to rotate the pyramid by,
        in degrees.
        :param axis: (Optional) An axis of rotation to rotate the pyramid around.
        If no axis is provided and the angle of rotation is nonzero, the pyramid
        will be rotated around the Y-axis.
        :param center: (Optional) The position of the centre of the base of the
        pyramid. If not provided, the pyramid will be placed on the coordinate
        origin.
        :param color: (Optional) The colour of the pyramid. If no colour is
        provided, a colour will be determined by the shader.
        """

        angle = math.radians(angle)

        minW = -width / 2
        maxW = width / 2
        minD = -depth / 2
        maxD = depth / 2

        start = self.getVertexCount()  #Starting index.

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        verts = numpy.asarray(
            [  #All 5 vertices of the pyramid.
                [minW, 0, maxD], [maxW, 0, maxD], [minW, 0, minD],
                [maxW, 0, minD], [0, height, 0]
            ],
            dtype=numpy.float32)
        verts = verts.dot(
            matrix.getData()[0:3, 0:3])  #Rotate the pyramid around the axis.
        verts[:] += center.getData()
        self.addVertices(verts)

        indices = numpy.asarray(
            [  #Connect the vertices to each other (6 triangles).
                [start, start + 1, start + 4],  #The four sides of the pyramid.
                [start + 1, start + 3, start + 4],
                [start + 3, start + 2, start + 4],
                [start + 2, start, start + 4],
                [start, start + 3, start + 1],  #The base of the pyramid.
                [start, start + 2, start + 3]
            ],
            dtype=numpy.int32)
        self.addIndices(indices)

        if color:  #If we have a colour, add the colour to each of the vertices.
            vertex_count = self.getVertexCount()
            for i in range(1, 6):
                self.setVertexColor(vertex_count - i, color)
예제 #31
0
 def test_setByTranslation(self):
     matrix = Matrix()
     matrix.setByTranslation(Vector(0,1,0))
     numpy.testing.assert_array_almost_equal(matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,1],[0,0,1,0],[0,0,0,1]]))
예제 #32
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))
예제 #33
0
 def test_transposed(self):
     temp_matrix = Matrix()  
     temp_matrix.setByTranslation(Vector(10,10,10))
     temp_matrix = temp_matrix.getTransposed()
     numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[10,10,10,1]]))
예제 #34
0
def transformVertices(vertices: numpy.ndarray, transformation: Matrix) -> numpy.ndarray:
    data = numpy.pad(vertices, ((0, 0), (0, 1)), "constant", constant_values=(0.0, 0.0))
    data = data.dot(transformation.getTransposed().getData())
    data += transformation.getData()[:, 3]
    data = data[:, 0:3]
    return data
예제 #35
0
class TestMatrix(unittest.TestCase):
    def setUp(self):
        self._matrix = Matrix()
        # Called before the first testfunction is executed
        pass

    def tearDown(self):
        # Called after the last testfunction was executed
        pass

    def test_setByQuaternion(self):
        pass

    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]]))

    def test_preMultiply(self):
        temp_matrix = Matrix()
        temp_matrix.setByTranslation(Vector(10, 10, 10))
        temp_matrix2 = Matrix()
        temp_matrix2.setByScaleFactor(0.5)
        temp_matrix.preMultiply(temp_matrix2)
        numpy.testing.assert_array_almost_equal(
            temp_matrix.getData(),
            numpy.array([[0.5, 0, 0, 5], [0, 0.5, 0, 5], [0, 0, 0.5, 5],
                         [0, 0, 0, 1]]))

    def test_setByScaleFactor(self):
        self._matrix.setByScaleFactor(0.5)
        numpy.testing.assert_array_almost_equal(
            self._matrix.getData(),
            numpy.array([[0.5, 0, 0, 0], [0, 0.5, 0, 0], [0, 0, 0.5, 0],
                         [0, 0, 0, 1]]))

    def test_setByRotation(self):
        pass

    def test_setByTranslation(self):
        self._matrix.setByTranslation(Vector(0, 1, 0))
        numpy.testing.assert_array_almost_equal(
            self._matrix.getData(),
            numpy.array([[1, 0, 0, 0], [0, 1, 0, 1], [0, 0, 1, 0],
                         [0, 0, 0, 1]]))

    def test_setToIdentity(self):
        pass

    def test_getData(self):
        pass

    def test_transposed(self):
        temp_matrix = Matrix()
        temp_matrix.setByTranslation(Vector(10, 10, 10))
        temp_matrix = temp_matrix.getTransposed()
        numpy.testing.assert_array_almost_equal(
            temp_matrix.getData(),
            numpy.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0],
                         [10, 10, 10, 1]]))

    def test_dot(self):
        pass