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
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()
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_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())
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 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
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)
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
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 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))
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
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)
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)
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
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)
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))
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 = []
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
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)
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)
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))
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)
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)
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
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))
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
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]))
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)
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]]))
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))
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]]))
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