def test_attributes(): attributes = { "test": { "value": [10], "derp": "OMG" }} mesh_data = MeshData(attributes = attributes) assert mesh_data.hasAttribute("test") assert mesh_data.getAttribute("test") == { "value": [10], "derp": "OMG" } assert mesh_data.attributeNames() == ["test"]
def test_transformMeshData(): transformation_matrix = Matrix() transformation_matrix.setByTranslation(Vector(30, 20, 10)) vertices = numpy.zeros((1, 3), dtype=numpy.float32) mesh_data = MeshData(vertices) transformed_mesh = mesh_data.getTransformed(transformation_matrix) assert transformed_mesh.getVertex(0)[0] == 30. assert transformed_mesh.getVertex(0)[1] == 20. assert transformed_mesh.getVertex(0)[2] == 10.
def run(self): objectIdMap = {} new_node = SceneNode() ## Put all nodes in a dict identified by ID for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if hasattr(node.getMeshData(), "layerData"): self._scene.getRoot().removeChild(node) else: objectIdMap[id(node)] = node settings = Application.getInstance().getActiveMachine() layerHeight = settings.getSettingValueByKey("layer_height") for object in self._message.objects: try: node = objectIdMap[object.id] except KeyError: continue mesh = MeshData() layerData = LayerData.LayerData() for layer in object.layers: for polygon in layer.polygons: points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. points = numpy.asarray(points, dtype=numpy.float32) points /= 1000 points = numpy.insert(points, 1, layer.id * layerHeight, axis = 1) points[:,2] *= -1 if not settings.getSettingValueByKey("machine_center_is_zero"): center = [settings.getSettingValueByKey("machine_width") / 2, 0.0, -settings.getSettingValueByKey("machine_depth") / 2] points -= numpy.array(center) #points = numpy.pad(points, ((0,0), (0,1)), "constant", constant_values=(0.0, 1.0)) #inverse = node.getWorldTransformation().getInverse().getData() #points = points.dot(inverse) #points = points[:,0:3] layerData.addPolygon(layer.id, polygon.type, points) # We are done processing all the layers we got from the engine, now create a mesh out of the data layerData.build() mesh.layerData = layerData new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot())
def test_hasData(): # Simple test to see if the has whatever functions do their job correctly empty_mesh = MeshData() assert not empty_mesh.hasNormals() assert not empty_mesh.hasColors() assert not empty_mesh.hasUVCoordinates() assert not empty_mesh.hasIndices() filled_mesh = MeshData(normals = [], colors = [], uvs = [], indices=[]) assert filled_mesh.hasNormals() assert filled_mesh.hasColors() assert filled_mesh.hasUVCoordinates() assert filled_mesh.hasIndices()
def read(self, file_name): self._dxf = DXFObjectReader.DXFObjectReader(open(file_name, "rt")) self._mesh = MeshData() for obj in self._dxf: if obj.getName() == "SECTION": if obj.get(2) == "ENTITIES": self._handleEntities() elif obj.get(2) == "TABLES": self._handleTables() else: Logger.log("d", "DXF: Got unknown section: %s", obj.get(2)) for obj in self._dxf: if obj.getName() == "ENDSEC": break else: Logger.log("d", "DXF: %s", obj) elif obj.getName() == "EOF": pass else: Logger.log("e", "DXF: Unexpected object: %s", obj) #self._mesh.calculateNormals() node = SceneNode() node.setMeshData(self._mesh) return node
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() renderer.setRenderSelection(False) if not self._material: self._material = renderer.createMaterial(Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "vertexcolor.frag")) self._material.setUniformValue("u_color", [1.0, 0.0, 0.0, 1.0]) self._selection_material = renderer.createMaterial(Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "color.frag")) self._selection_material.setUniformValue("u_color", Color(35, 35, 35, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, material = self._selection_material, transparent = True) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh = layer_data, material = self._material, mode = Renderer.RenderLines, start = start, end = end) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue layer_mesh = layer_data.getLayer(layer).createMesh() if not layer_mesh or layer_mesh.getVertices() is None: continue self._current_layer_mesh.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_mesh.addColors(layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh = self._current_layer_mesh, material = self._material)
def run(self): layer_data = None for node in DepthFirstIterator(self._scene.getRoot()): layer_data = node.callDecoration("getLayerData") if layer_data: break if self._cancel or not layer_data: return layer_mesh = MeshData() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: continue try: layer = layer_data.getLayer(layer_number).createMesh() except Exception as e: print(e) return if not layer or layer.getVertices() is None: continue layer_mesh.addVertices(layer.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 layer_mesh.addColors(layer.getColors() * brightness) if self._cancel: return Job.yieldThread() if self._cancel: return Job.yieldThread() jump_mesh = layer_data.getLayer(self._layer_number).createJumps() if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None self.setResult({ "layers": layer_mesh, "jumps": jump_mesh })
def read(self, file_name, storage_device): mesh = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: mesh = MeshData() f = storage_device.openFile(file_name, "rb") if not self._loadBinary(mesh, f): storage_device.closeFile(f) f = storage_device.openFile(file_name, "rt") try: self._loadAscii(mesh, f) except UnicodeDecodeError: pass storage_device.closeFile(f) storage_device.closeFile(f) mesh.calculateNormals() Logger.log("d", "Loaded a mesh with %s vertices", mesh.getVertexCount()) return mesh
def build(self) -> MeshData: """Build a MeshData object. :return: A Mesh data. """ return MeshData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(), colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(), center_position=self.getCenterPosition())
def read(self, file_name): mesh = None scene_node = None mesh = MeshData() scene_node = SceneNode() f = open(file_name, "rb") if not self._loadBinary(mesh, f): f.close() f = open(file_name, "rt") try: self._loadAscii(mesh, f) except UnicodeDecodeError: pass f.close() f.close() time.sleep(0.1) #Yield somewhat to ensure the GUI has time to update a bit. mesh.calculateNormals(fast = True) Logger.log("d", "Loaded a mesh with %s vertices", mesh.getVertexCount()) scene_node.setMeshData(mesh) return scene_node
def read(self, file_name): mesh = None scene_node = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: mesh = MeshData() scene_node = SceneNode() f = open(file_name, "rb") if not self._loadBinary(mesh, f): f.close() f = open(file_name, "rt") try: self._loadAscii(mesh, f) except UnicodeDecodeError: pass f.close() f.close() mesh.calculateNormals(fast = True) Logger.log("d", "Loaded a mesh with %s vertices", mesh.getVertexCount()) scene_node.setMeshData(mesh) return scene_node
def read(self, file_name): mesh = None scene_node = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: mesh = MeshData() scene_node = SceneNode() f = open(file_name, "rb") if not self._loadBinary(mesh, f): f.close() f = open(file_name, "rt") try: self._loadAscii(mesh, f) except UnicodeDecodeError: pass f.close() f.close() time.sleep(0.1) #Yield somewhat to ensure the GUI has time to update a bit. mesh.calculateNormals(fast = True) Logger.log("d", "Loaded a mesh with %s vertices", mesh.getVertexCount()) scene_node.setMeshData(mesh) return scene_node
def createHullMesh(self, hull_points): mesh = MeshData() if len(hull_points) > 3: center = (hull_points.min(0) + hull_points.max(0)) / 2.0 mesh.addVertex(center[0], -0.1, center[1]) else: return None for point in hull_points: mesh.addVertex(point[0], -0.1, point[1]) indices = [] for i in range(len(hull_points) - 1): indices.append([0, i + 1, i + 2]) indices.append([0, mesh.getVertexCount() - 1, 1]) mesh.addIndices(numpy.array(indices, numpy.int32)) return mesh
def test_deepCopy(self): node_1 = SceneNode() node_2 = SceneNode() node_1.translate(Vector(1, 2, 3)) node_1.scale(Vector(1.5, 1., 1.)) node_1.setMeshData(MeshData()) node_1.addChild(node_2) node_1.addDecorator(GroupDecorator()) copied_node = deepcopy(node_1) assert copied_node.getScale() == Vector(1.5, 1, 1) assert copied_node.getPosition() == Vector(1, 2, 3) assert len(copied_node.getChildren()) == 1 # Ensure that the decorator also got copied assert copied_node.callDecoration("isGroup")
def setPaths(self, engrave_paths, cut_paths): self._cut_paths = cut_paths self._engrave_paths = engrave_paths self._mesh = MeshData() last_point = None for path in engrave_paths.closed_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._engrave_color) last_point = point self._addMeshLine(last_point, path[0], self._engrave_color) last_point = path[0] for path in engrave_paths.open_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._engrave_color) last_point = point for path in cut_paths.closed_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._cut_color) last_point = point self._addMeshLine(last_point, path[0], self._cut_color) last_point = path[0] for path in cut_paths.open_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._cut_color) last_point = point self.getNode().setMeshData(self._mesh)
def test_counts(): empty_mesh = MeshData() assert empty_mesh.getFaceCount() == 0 assert empty_mesh.getVertexCount() == 0 filled_mesh = MeshData(indices = numpy.zeros((5, 3), dtype=numpy.float32), vertices = numpy.zeros((12, 3), dtype=numpy.float32)) assert filled_mesh.getFaceCount() == 5 assert filled_mesh.getVertexCount() == 12
def __init__(self, node, hull, parent = None): super().__init__(parent) self.setCalculateBoundingBox(False) self._material = None self._original_parent = parent self._inherit_orientation = False self._inherit_scale = False self._node = node self._node.transformationChanged.connect(self._onNodePositionChanged) self._node.parentChanged.connect(self._onNodeParentChanged) #self._onNodePositionChanged(self._node) self._hull = hull hull_points = self._hull.getPoints() mesh = MeshData() if len(hull_points) > 3: center = (hull_points.min(0) + hull_points.max(0)) / 2.0 mesh.addVertex(center[0], 0.1, center[1]) else: #Hull has not enough points return for point in hull_points: mesh.addVertex(point[0], 0.1, point[1]) indices = [] for i in range(len(hull_points) - 1): indices.append([0, i + 1, i + 2]) indices.append([0, mesh.getVertexCount() - 1, 1]) mesh.addIndices(numpy.array(indices, numpy.int32)) self.setMeshData(mesh)
def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB()
def __init__(self, node, hull, parent = None): super().__init__(parent) self.setCalculateBoundingBox(False) self._material = None self._original_parent = parent self._inherit_orientation = False self._inherit_scale = False self._node = node self._node.transformationChanged.connect(self._onNodePositionChanged) self._node.parentChanged.connect(self._onNodeParentChanged) #self._onNodePositionChanged(self._node) self._hull = hull hull_points = self._hull.getPoints() center = (hull_points.min(0) + hull_points.max(0)) / 2.0 mesh = MeshData() mesh.addVertex(center[0], 0.1, center[1]) for point in hull_points: mesh.addVertex(point[0], 0.1, point[1]) indices = [] for i in range(len(hull_points) - 1): indices.append([0, i + 1, i + 2]) indices.append([0, mesh.getVertexCount() - 1, 1]) mesh.addIndices(numpy.array(indices, numpy.int32)) self.setMeshData(mesh)
def run(self): layer_data = None for node in DepthFirstIterator(self._scene.getRoot()): layer_data = node.callDecoration("getLayerData") if layer_data: break if self._cancel or not layer_data: return layer_mesh = MeshData() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: continue try: layer = layer_data.getLayer(layer_number).createMesh() except Exception as e: print(e) return if not layer or layer.getVertices() is None: continue layer_mesh.addVertices(layer.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 layer_mesh.addColors(layer.getColors() * brightness) if self._cancel: return Job.yieldThread() if self._cancel: return Job.yieldThread() jump_mesh = layer_data.getLayer(self._layer_number).createJumps() if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None self.setResult({"layers": layer_mesh, "jumps": jump_mesh})
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: """Converts a Trimesh to Uranium's MeshData. :param tri_node: A Trimesh containing the contents of a file that was just read. :param file_name: The full original filename used to watch for changes :return: Mesh data from the Trimesh in a way that Uranium can understand it. """ tri_faces = tri_node.faces tri_vertices = tri_node.vertices indices_list = [] vertices_list = [] index_count = 0 face_count = 0 for tri_face in tri_faces: face = [] for tri_index in tri_face: vertices_list.append(tri_vertices[tri_index]) face.append(index_count) index_count += 1 indices_list.append(face) face_count += 1 vertices = numpy.asarray(vertices_list, dtype=numpy.float32) indices = numpy.asarray(indices_list, dtype=numpy.int32) normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count) mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals, file_name=file_name) return mesh_data
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData: tri_faces = tri_node.faces tri_vertices = tri_node.vertices indices = [] vertices = [] index_count = 0 face_count = 0 for tri_face in tri_faces: face = [] for tri_index in tri_face: vertices.append(tri_vertices[tri_index]) face.append(index_count) index_count += 1 indices.append(face) face_count += 1 vertices = numpy.asarray(vertices, dtype=numpy.float32) indices = numpy.asarray(indices, dtype=numpy.int32) normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count) mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals) return mesh_data
# Copyright (c) 2015 Ultimaker B.V. # Uranium is released under the terms of the LGPLv3 or higher. from UM.Mesh.MeshData import MeshData @profile def calcNormals(mesh): mesh.calculateNormals() mesh = MeshData() mesh.reserveVertexCount(99999) for i in range(33333): mesh.addVertex(0, 1, 0) mesh.addVertex(1, 1, 0) mesh.addVertex(1, 0, 0) for i in range(100): calcNormals(mesh)
class MeshBuilder: ## Creates a new MeshBuilder with an empty mesh. def __init__(self): self._mesh_data = MeshData() ## Gets the mesh that was built by this MeshBuilder. # # Note that this gets a reference to the mesh. Adding more primitives to # the MeshBuilder will cause the mesh returned by this function to change # as well. # # \return A mesh with all the primitives added to this MeshBuilder. def getData(self): return self._mesh_data ## Adds a 3-dimensional line to the mesh of this mesh builder. # # \param v0 One endpoint of the line to add. # \param v1 The other endpoint of the line to add. # \param color (Optional) The colour of the line, if any. If no colour is # provided, the colour is determined by the shader. def addLine(self, v0, v1, color = None): self._mesh_data.addVertex(v0.x, v0.y, v0.z) self._mesh_data.addVertex(v1.x, v1.y, v1.z) if color: #Add colours to the vertices, if we have them. self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) ## Adds a triangle to the mesh of this mesh builder. # # \param v0 The first corner of the triangle. # \param v1 The second corner of the triangle. # \param v2 The third corner of the triangle. # \param normal (Optional) The normal vector for the triangle. If no # normal vector is provided, it will be calculated automatically. # \param color (Optional) The colour for the triangle. If no colour is # provided, the colour is determined by the shader. def addFace(self, v0, v1, v2, normal = None, color = None): if normal: self._mesh_data.addFaceWithNormals( v0.x, v0.y, v0.z, normal.x, normal.y, normal.z, v1.x, v1.y, v1.z, normal.x, normal.y, normal.z, v2.x, v2.y, v2.z, normal.x, normal.y, normal.z ) else: self._mesh_data.addFace(v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z) #Computes the normal by itself. if color: #Add colours to the vertices if we have them. self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 3, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) ## Add a quadrilateral to the mesh of this mesh builder. # # The quadrilateral will be constructed as two triangles. v0 and v2 are # the two vertices across the diagonal of the quadrilateral. # # \param v0 The first corner of the quadrilateral. # \param v1 The second corner of the quadrilateral. # \param v2 The third corner of the quadrilateral. # \param v3 The fourth corner of the quadrilateral. # \param normal (Optional) The normal vector for the quadrilateral. Both # triangles will get the same normal vector, if provided. If no normal # vector is provided, the normal vectors for both triangles are computed # automatically. # \param color (Optional) The colour for the quadrilateral. If no colour # is provided, the colour is determined by the shader. def addQuad(self, v0, v1, v2, v3, normal = None, color = None): self.addFace(v0, v2, v1, color = color, normal = normal ) self.addFace(v0, v3, v2, #v0 and v2 are shared with the other triangle! color = color, normal = normal ) ## Add a rectangular cuboid to the mesh of this mesh builder. # # A rectangular cuboid is a square block with arbitrary width, height and # depth. # # \param width The size of the rectangular cuboid in the X dimension. # \param height The size of the rectangular cuboid in the Y dimension. # \param depth The size of the rectangular cuboid in the Z dimension. # \param center (Optional) The position of the centre of the rectangular # cuboid in space. If not provided, the cuboid is placed at the coordinate # origin. # \param color (Optional) The colour for the rectangular cuboid. If no # colour is provided, the colour is determined by the shader. def addCube(self, width, height, depth, center = Vector(0, 0, 0), color = None): #Compute the actual positions of the planes. minW = -width / 2 + center.x maxW = width / 2 + center.x minH = -height / 2 + center.y maxH = height / 2 + center.y minD = -depth / 2 + center.z maxD = depth / 2 + center.z start = self._mesh_data.getVertexCount() verts = numpy.asarray([ #All 8 corners. [minW, minH, maxD], [minW, maxH, maxD], [maxW, maxH, maxD], [maxW, minH, maxD], [minW, minH, minD], [minW, maxH, minD], [maxW, maxH, minD], [maxW, minH, minD], ], dtype=numpy.float32) self._mesh_data.addVertices(verts) indices = numpy.asarray([ #All 6 quads (12 triangles). [start, start + 2, start + 1], [start, start + 3, start + 2], [start + 3, start + 7, start + 6], [start + 3, start + 6, start + 2], [start + 7, start + 5, start + 6], [start + 7, start + 4, start + 5], [start + 4, start + 1, start + 5], [start + 4, start + 0, start + 1], [start + 1, start + 6, start + 5], [start + 1, start + 2, start + 6], [start + 0, start + 7, start + 3], [start + 0, start + 4, start + 7] ], dtype=numpy.int32) self._mesh_data.addIndices(indices) if color: #If we have a colour, add a colour to all of the vertices. vertex_count = self._mesh_data.getVertexCount() for i in range(1, 9): self._mesh_data.setVertexColor(vertex_count - i, color) ## Add an arc to the mesh of this mesh builder. # # An arc is a curve that is also a segment of a circle. # # \param radius The radius of the circle this arc is a segment of. # \param axis The axis perpendicular to the plane on which the arc lies. # \param angle (Optional) The length of the arc, in radians. If not # provided, the entire circle is used (2 pi). # \param center (Optional) The position of the centre of the arc in space. # If no position is provided, the arc is centred around the coordinate # origin. # \param sections (Optional) The resolution of the arc. The arc is # approximated by this number of line segments. # \param color (Optional) The colour for the arc. If no colour is # provided, the colour is determined by the shader. def addArc(self, radius, axis, angle = math.pi * 2, center = Vector(0, 0, 0), sections = 32, color = None): #We'll compute the vertices of the arc by computing an initial point and #rotating the initial point with a rotation matrix. if axis == Vector.Unit_Y: start = axis.cross(Vector.Unit_X).normalize() * radius else: start = axis.cross(Vector.Unit_Y).normalize() * radius angle_increment = angle / sections current_angle = 0 point = start + center m = Matrix() while current_angle <= angle: #Add each of the vertices. self._mesh_data.addVertex(point.x, point.y, point.z) current_angle += angle_increment m.setByRotationAxis(current_angle, axis) point = start.multiply(m) + center #Get the next vertex by rotating the start position with a matrix. self._mesh_data.addVertex(point.x, point.y, point.z) if color: #If we have a colour, add that colour to the new vertex. self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) ## 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. 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._mesh_data.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._mesh_data.addVertices(vertices) self._mesh_data.addIndices(numpy.asarray(indices, dtype = numpy.int32)) self._mesh_data.addColors(numpy.asarray(colors, dtype = numpy.float32)) ## 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. 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._mesh_data.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._mesh_data.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._mesh_data.addIndices(indices) if color: #If we have a colour, add the colour to each of the vertices. vertex_count = self._mesh_data.getVertexCount() for i in range(1, 6): self._mesh_data.setVertexColor(vertex_count - i, color)
def _rebuild(self): lines = MeshData() offset = 0 if self.YAxis in self._enabled_axis: lines.addVertex(0, 0, 0) lines.addVertex(0, 20, 0) lines.setVertexColor(offset, ToolHandle.YAxisColor) lines.setVertexColor(offset + 1, ToolHandle.YAxisColor) offset += 2 if self.XAxis in self._enabled_axis: lines.addVertex(0, 0, 0) lines.addVertex(20, 0, 0) lines.setVertexColor(offset, ToolHandle.XAxisColor) lines.setVertexColor(offset + 1, ToolHandle.XAxisColor) offset += 2 if self.ZAxis in self._enabled_axis: lines.addVertex(0, 0, 0) lines.addVertex(0, 0, 20) lines.setVertexColor(offset, ToolHandle.ZAxisColor) lines.setVertexColor(offset + 1, ToolHandle.ZAxisColor) offset += 2 self.setLineMesh(lines) mb = MeshBuilder() if self.YAxis in self._enabled_axis: mb.addPyramid( width = 2, height = 4, depth = 2, center = Vector(0, 20, 0), color = ToolHandle.YAxisColor ) if self.XAxis in self._enabled_axis: mb.addPyramid( width = 2, height = 4, depth = 2, center = Vector(20, 0, 0), color = ToolHandle.XAxisColor, axis = Vector.Unit_Z, angle = 90 ) if self.ZAxis in self._enabled_axis: mb.addPyramid( width = 2, height = 4, depth = 2, center = Vector(0, 0, 20), color = ToolHandle.ZAxisColor, axis = Vector.Unit_X, angle = -90 ) self.setSolidMesh(mb.getData()) mb = MeshBuilder() if self.YAxis in self._enabled_axis: mb.addCube( width = 4, height = 20, depth = 4, center = Vector(0, 10, 0), color = ToolHandle.YAxisColor ) mb.addPyramid( width = 4, height = 8, depth = 4, center = Vector(0, 20, 0), color = ToolHandle.YAxisColor ) if self.XAxis in self._enabled_axis: mb.addCube( width = 20, height = 4, depth = 4, center = Vector(10, 0, 0), color = ToolHandle.XAxisColor ) mb.addPyramid( width = 4, height = 8, depth = 4, center = Vector(20, 0, 0), color = ToolHandle.XAxisColor, axis = Vector.Unit_Z, angle = 90 ) if self.ZAxis in self._enabled_axis: mb.addCube( width = 4, height = 4, depth = 20, center = Vector(0, 0, 10), color = ToolHandle.ZAxisColor ) mb.addPyramid( width = 4, height = 8, depth = 4, center = Vector(0, 0, 20), color = ToolHandle.ZAxisColor, axis = Vector.Unit_X, angle = -90 ) self.setSelectionMesh(mb.getData())
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent = None, name = ""): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._mirror = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = name self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._orientation = self._orientation * rotation elif transform_space == SceneNode.TransformSpace.Parent: self._orientation = rotation * self._orientation elif transform_space == SceneNode.TransformSpace.World: self._orientation = self._orientation * self._getDerivedOrientation().getInverse() * rotation * self._getDerivedOrientation() else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._orientation.normalize() self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. def setOrientation(self, orientation): if not self._enabled or orientation == self._orientation: return self._orientation = orientation self._orientation.normalize() self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._scale = self._scale.scale(scale) elif transform_space == SceneNode.TransformSpace.Parent: raise NotImplementedError() elif transform_space == SceneNode.TransformSpace.World: if self._parent: scale_change = Vector(1,1,1) - scale if scale_change.x < 0 or scale_change.y < 0 or scale_change.z < 0: direction = -1 else: direction = 1 # Hackish way to do this, but this seems to correctly scale the object. change_vector = self._scale.scale(self._getDerivedOrientation().getInverse().rotate(scale_change)) if change_vector.x < 0 and direction == 1: change_vector.setX(-change_vector.x) if change_vector.x > 0 and direction == -1: change_vector.setX(-change_vector.x) if change_vector.y < 0 and direction == 1: change_vector.setY(-change_vector.y) if change_vector.y > 0 and direction == -1: change_vector.setY(-change_vector.y) if change_vector.z < 0 and direction == 1: change_vector.setZ(-change_vector.z) if change_vector.z > 0 and direction == -1: change_vector.setZ(-change_vector.z) self._scale -= self._scale.scale(change_vector) else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. def setScale(self, scale): if not self._enabled or scale == self._scale: return self._scale = scale self._transformChanged() ## Get the local mirror values. # # \return The mirror values as vector of scaling values. def getMirror(self): return deepcopy(self._mirror) ## Mirror the scene object (and thus its children) in the given directions. # # \param mirror \type{Vector} A vector of three scale values that is used # to mirror the node. # \param transform_space The space relative to which to scale. Can be any # one of the constants in SceneNode::TransformSpace. def mirror(self, mirror, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.Parent: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.World: self._mirror *= mirror else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local mirror values. # # \param mirror \type{Vector} The new mirror values as scale multipliers. def setMirror(self, mirror): if not self._enabled or mirror == self._mirror: return self._mirror = mirror self._transformChanged() ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): if not self._derived_position: self._updateTransformation() return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._position += self._orientation.rotate(translation) elif transform_space == SceneNode.TransformSpace.Parent: self._position += translation elif transform_space == SceneNode.TransformSpace.World: if self._parent: self._position += (1.0 / self._parent._getDerivedScale()).scale(self._parent._getDerivedOrientation().getInverse().rotate(translation)) else: self._position += translation self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. def setPosition(self, position): if not self._enabled or position == self._position: return self._position = position self._transformChanged() ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) if self._parent: self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged() ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _getDerivedPosition(self): if not self._derived_position: self._updateTransformation() return self._derived_position def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() return self._derived_orientation def _getDerivedScale(self): if not self._derived_scale: self._updateTransformation() return self._derived_scale def _transformChanged(self): self._resetAABB() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale_and_mirror = self._scale * self._mirror self._transformation = Matrix.fromPositionOrientationScale(self._position, self._orientation, scale_and_mirror) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(scale_and_mirror) else: self._derived_scale = scale_and_mirror self._derived_position = parent_orientation.rotate(parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale(self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = scale_and_mirror self._world_transformation = self._transformation def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
def read(self, file_name): mesh = None scene_node = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: vertex_list = [] normal_list = [] uv_list = [] face_list = [] scene_node = SceneNode() mesh = MeshData() scene_node.setMeshData(mesh) f = open(file_name, "rt") for line in f: parts = line.split() if len(parts) < 1: continue if parts[0] == "v": vertex_list.append([float(parts[1]), float(parts[3]), -float(parts[2])]) if parts[0] == "vn": normal_list.append([float(parts[1]), float(parts[3]), -float(parts[2])]) if parts[0] == "vt": uv_list.append([float(parts[1]), float(parts[2])]) if parts[0] == "f": parts = [i for i in map(lambda p: p.split("/"), parts)] for idx in range(1, len(parts)-2): data = [int(parts[1][0]), int(parts[idx+1][0]), int(parts[idx+2][0])] if len(parts[1]) > 2: data += [int(parts[1][2]), int(parts[idx+1][2]), int(parts[idx+2][2])] if parts[1][1] and parts[idx+1][1] and parts[idx+2][1]: data += [int(parts[1][1]), int(parts[idx+1][1]), int(parts[idx+2][1])] face_list.append(data) f.close() mesh.reserveVertexCount(3 * len(face_list)) num_vertices = len(vertex_list) num_normals = len(normal_list) for face in face_list: # Substract 1 from index, as obj starts counting at 1 instead of 0 i = face[0] - 1 j = face[1] - 1 k = face[2] - 1 if len(face) > 3: ni = face[3] - 1 nj = face[4] - 1 nk = face[5] - 1 else: ni = -1 nj = -1 nk = -1 if len(face) > 6: ui = face[6] - 1 uj = face[7] - 1 uk = face[8] - 1 else: ui = -1 uj = -1 uk = -1 #TODO: improve this handling, this can cause weird errors if i < 0 or i >= num_vertices: i = 0 if j < 0 or j >= num_vertices: j = 0 if k < 0 or k >= num_vertices: k = 0 if(ni != -1 and nj != -1 and nk != -1): mesh.addFaceWithNormals(vertex_list[i][0], vertex_list[i][1], vertex_list[i][2], normal_list[ni][0], normal_list[ni][1], normal_list[ni][2], vertex_list[j][0], vertex_list[j][1], vertex_list[j][2], normal_list[nj][0], normal_list[nj][1], normal_list[nj][2], vertex_list[k][0], vertex_list[k][1], vertex_list[k][2],normal_list[nk][0], normal_list[nk][1], normal_list[nk][2]) else: mesh.addFace(vertex_list[i][0], vertex_list[i][1], vertex_list[i][2], vertex_list[j][0], vertex_list[j][1], vertex_list[j][2], vertex_list[k][0], vertex_list[k][1], vertex_list[k][2]) if ui != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 3, uv_list[ui][0], uv_list[ui][1]) if uj != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 2, uv_list[uj][0], uv_list[uj][1]) if uk != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 1, uv_list[uk][0], uv_list[uk][1]) if not mesh.hasNormals(): mesh.calculateNormals(fast = True) return scene_node
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): mesh = None scene_node = None scene_node = SceneNode() mesh = MeshData() scene_node.setMeshData(mesh) img = QImage(file_name) if img.isNull(): Logger.log("e", "Image is corrupt.") return None width = max(img.width(), 2) height = max(img.height(), 2) aspect = height / width if img.width() < 2 or img.height() < 2: img = img.scaled(width, height, Qt.IgnoreAspectRatio) base_height = max(base_height, 0) peak_height = max(peak_height, -base_height) xz_size = max(xz_size, 1) scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: scale_vector.setZ(scale_vector.z * aspect) elif height > width: scale_vector.setX(scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width if height > width: scale_factor = max_size / height width = int(max(round(width * scale_factor), 2)) height = int(max(round(height * scale_factor), 2)) img = img.scaled(width, height, Qt.IgnoreAspectRatio) width_minus_one = width - 1 height_minus_one = height - 1 Job.yieldThread() texel_width = 1.0 / (width_minus_one) * scale_vector.x texel_height = 1.0 / (height_minus_one) * scale_vector.z height_data = numpy.zeros((height, width), dtype=numpy.float32) for x in range(0, width): for y in range(0, height): qrgb = img.pixel(x, y) avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255) height_data[y, x] = avg Job.yieldThread() if image_color_invert: height_data = 1 - height_data for i in range(0, blur_iterations): copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge") height_data += copy[1:-1, 2:] height_data += copy[1:-1, :-2] height_data += copy[2:, 1:-1] height_data += copy[:-2, 1:-1] height_data += copy[2:, 2:] height_data += copy[:-2, 2:] height_data += copy[2:, :-2] height_data += copy[:-2, :-2] height_data /= 9 Job.yieldThread() height_data *= scale_vector.y height_data += base_height heightmap_face_count = 2 * height_minus_one * width_minus_one total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2 mesh.reserveFaceCount(total_face_count) # initialize to texel space vertex offsets. # 6 is for 6 vertices for each texel quad. heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype = numpy.float32) heightmap_vertices = heightmap_vertices + numpy.array([[ [0, base_height, 0], [0, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, 0], [0, base_height, 0] ]], dtype = numpy.float32) offsetsz, offsetsx = numpy.mgrid[0: height_minus_one, 0: width - 1] offsetsx = numpy.array(offsetsx, numpy.float32).reshape(-1, 1) * texel_width offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height # offsets for each texel quad heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1) heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3) # apply height data to y values heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, :-1].reshape(-1) heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1) heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1) heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1) heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3) mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3) mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices mesh._vertex_count = heightmap_vertices.size // 3 mesh._face_count = heightmap_indices.size // 3 geo_width = width_minus_one * texel_width geo_height = height_minus_one * texel_height # bottom mesh.addFace(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) mesh.addFace(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) # north and south walls for n in range(0, width_minus_one): x = n * texel_width nx = (n + 1) * texel_width hn0 = height_data[0, n] hn1 = height_data[0, n + 1] hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] mesh.addFace(x, 0, 0, nx, 0, 0, nx, hn1, 0) mesh.addFace(nx, hn1, 0, x, hn0, 0, x, 0, 0) mesh.addFace(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) mesh.addFace(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) # west and east walls for n in range(0, height_minus_one): y = n * texel_height ny = (n + 1) * texel_height hw0 = height_data[n, 0] hw1 = height_data[n + 1, 0] he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] mesh.addFace(0, 0, y, 0, 0, ny, 0, hw1, ny) mesh.addFace(0, hw1, ny, 0, hw0, y, 0, 0, y) mesh.addFace(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) mesh.addFace(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) mesh.calculateNormals(fast=True) return scene_node
def run(self): start_time = time() if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break if self._abort_requested: if self._progress: self._progress.hide() return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1,1)) points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1,3)) line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # In the future, line_thicknesses should be given by CuraEngine as well. # Currently the infill layer thickness also translates to line width line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance().getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list(manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except KeyError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry("color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent(new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When disabling the remove empty first layers setting, the minimum layer number will be a positive # value. In that case the first empty layers will be discarded and start processing layers from the # first layer with data. # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # simply offset all other layers so the lowest layer is always 0. It could happens that the first # raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = sys.maxsize negative_layers = 0 for layer in self._layers: if layer.repeatedMessageCount("path_segment") > 0: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # data. However, if there are empty layers in between, we compute them. if layer.id < min_layer_number: continue # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # and the model abs_layer_number = layer.id - min_layer_number if layer.id >= 0 and negative_layers != 0: abs_layer_number += (min_layer_number + negative_layers) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = manager.getActiveExtruderStacks() if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool( Application.getInstance().getPreferences().getValue( "view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time)
def build(self): return MeshData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(), colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(), center_position=self.getCenterPosition())
class LayerView(View): def __init__(self): super().__init__() self._shader = None self._selection_shader = None self._num_layers = 0 self._layer_percentage = 0 # what percentage of layers need to be shown (SLider gives value between 0 - 100) self._proxy = LayerViewProxy.LayerViewProxy() self._controller.getScene().sceneChanged.connect(self._onSceneChanged) self._max_layers = 10 self._current_layer_num = 10 self._current_layer_mesh = None self._current_layer_jumps = None self._activity = False self._solid_layers = 5 def getActivity(self): return self._activity def getCurrentLayer(self): return self._current_layer_num def _onSceneChanged(self, node): self.calculateMaxLayers() def getMaxLayers(self): return self._max_layers def resetLayerData(self): self._current_layer_mesh = None self._current_layer_jumps = None def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._selection_shader: self._selection_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader")) self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, transparent = True, shader = self._selection_shader) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh = layer_data, mode = RenderBatch.RenderMode.Lines, range = (start, end)) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer(layer).createMesh() if not layer_mesh or layer_mesh.getVertices() is None: continue except: continue if self._current_layer_mesh: #Threading thing; Switching between views can cause the current layer mesh to be deleted. self._current_layer_mesh.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 if self._current_layer_mesh: self._current_layer_mesh.addColors(layer_mesh.getColors() * brightness) if self._current_layer_mesh: renderer.queueNode(node, mesh = self._current_layer_mesh) if not self._current_layer_jumps: self._current_layer_jumps = MeshData() for i in range(1): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer(layer).createJumps() if not layer_mesh or layer_mesh.getVertices() is None: continue except: continue self._current_layer_jumps.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_jumps.addColors(layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh = self._current_layer_jumps) def setLayer(self, value): if self._current_layer_num != value: self._current_layer_num = value if self._current_layer_num < 0: self._current_layer_num = 0 if self._current_layer_num > self._max_layers: self._current_layer_num = self._max_layers self._current_layer_mesh = None self._current_layer_jumps = None self.currentLayerNumChanged.emit() currentLayerNumChanged = Signal() def calculateMaxLayers(self): scene = self.getController().getScene() renderer = self.getRenderer() self._activity = True self._old_max_layers = self._max_layers ## Recalculate num max layers new_max_layers = 0 for node in DepthFirstIterator(scene.getRoot()): layer_data = node.callDecoration("getLayerData") if not layer_data: continue if new_max_layers < len(layer_data.getLayers()): new_max_layers = len(layer_data.getLayers()) - 1 if new_max_layers > 0 and new_max_layers != self._old_max_layers: self._max_layers = new_max_layers # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first # if it's the largest value. If we don't do this, we can have a slider block outside of the # slider. if new_max_layers > self._current_layer_num: self.maxLayersChanged.emit() self.setLayer(int(self._max_layers)) else: self.setLayer(int(self._max_layers)) self.maxLayersChanged.emit() maxLayersChanged = Signal() currentLayerNumChanged = Signal() ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): return self._proxy def endRendering(self): pass def event(self, event): modifiers = QtWidgets.QApplication.keyboardModifiers() ctrl_is_active = modifiers == QtCore.Qt.ControlModifier if event.type == Event.KeyPressEvent and ctrl_is_active: if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + 1) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - 1) return True
# Copyright (c) 2015 Ultimaker B.V. # Uranium is released under the terms of the LGPLv3 or higher. from UM.Mesh.MeshData import MeshData @profile def addVertex(mesh): mesh.addVertex(0, 1, 0) mesh = MeshData() mesh.reserveVertexCount(1000000) for i in range(1000000): addVertex(mesh)
def read(self, file_name): result = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) if len(objects) == 0: Logger.log( "w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) return None for object in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in object.mesh.vertices.vertex: for vertex in object.findall(".//3mf:vertex", self._namespaces): vertex_list.append([ vertex.get("x"), vertex.get("y"), vertex.get("z") ]) Job.yieldThread() triangles = object.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2], vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2], vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2]) Job.yieldThread() #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall( "./3mf:build/3mf:item[@objectid='{0}']".format( object.get("id")), self._namespaces) if transformation: transformation = transformation[0] if transformation.get("transform"): splitted_transformation = transformation.get( "transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0, 0] = splitted_transformation[0] temp_mat._data[1, 0] = splitted_transformation[1] temp_mat._data[2, 0] = splitted_transformation[2] temp_mat._data[0, 1] = splitted_transformation[3] temp_mat._data[1, 1] = splitted_transformation[4] temp_mat._data[2, 1] = splitted_transformation[5] temp_mat._data[0, 2] = splitted_transformation[6] temp_mat._data[1, 2] = splitted_transformation[7] temp_mat._data[2, 2] = splitted_transformation[8] # Translation temp_mat._data[0, 3] = splitted_transformation[9] temp_mat._data[1, 3] = splitted_transformation[10] temp_mat._data[2, 3] = splitted_transformation[11] node.setPosition( Vector(temp_mat.at(0, 3), temp_mat.at(1, 3), temp_mat.at(2, 3))) temp_quaternion = Quaternion() temp_quaternion.setByMatrix(temp_mat) node.setOrientation(temp_quaternion) # Magical scale extraction scale = temp_mat.getTransposed().multiply(temp_mat) scale_x = math.sqrt(scale.at(0, 0)) scale_y = math.sqrt(scale.at(1, 1)) scale_z = math.sqrt(scale.at(2, 2)) node.setScale(Vector(scale_x, scale_y, scale_z)) # We use a different coordinate frame, so rotate. #rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0)) #node.rotate(rotation) result.addChild(node) Job.yieldThread() #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e", "exception occured in 3mf reader: %s", e) return result
# Copyright (c) 2015 Ultimaker B.V. # Uranium is released under the terms of the LGPLv3 or higher. from UM.Mesh.MeshData import MeshData @profile def getByteArray(mesh): return mesh.getVerticesAsByteArray() mesh = MeshData() mesh.reserveVertexCount(10000) for i in range(10000): mesh.addVertex(0, 1, 0) for i in range(100): a = getByteArray(mesh)
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() renderer.setRenderSelection(False) if not self._material: self._material = renderer.createMaterial( Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "vertexcolor.frag")) self._material.setUniformValue("u_color", [1.0, 0.0, 0.0, 1.0]) self._selection_material = renderer.createMaterial( Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "color.frag")) self._selection_material.setUniformValue("u_color", Color(35, 35, 35, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected( node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, material=self._selection_material, transparent=True) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh=layer_data, material=self._material, mode=Renderer.RenderLines, start=start, end=end) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer( layer).createMesh() if not layer_mesh or layer_mesh.getVertices( ) is None: continue except: continue if self._current_layer_mesh: #Threading thing; Switching between views can cause the current layer mesh to be deleted. self._current_layer_mesh.addVertices( layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 if self._current_layer_mesh: self._current_layer_mesh.addColors( layer_mesh.getColors() * brightness) if self._current_layer_mesh: renderer.queueNode(node, mesh=self._current_layer_mesh, material=self._material) if not self._current_layer_jumps: self._current_layer_jumps = MeshData() for i in range(1): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer( layer).createJumps() if not layer_mesh or layer_mesh.getVertices( ) is None: continue except: continue self._current_layer_jumps.addVertices( layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_jumps.addColors( layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh=self._current_layer_jumps, material=self._material)
def test_hasData(): # Simple test to see if the has whatever functions do their job correctly empty_mesh = MeshData() assert not empty_mesh.hasNormals() assert not empty_mesh.hasColors() assert not empty_mesh.hasUVCoordinates() assert not empty_mesh.hasIndices() filled_mesh = MeshData(normals=[], colors=[], uvs=[], indices=[]) assert filled_mesh.hasNormals() assert filled_mesh.hasColors() assert filled_mesh.hasUVCoordinates() assert filled_mesh.hasIndices()
def run(self): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("Layers View mode", "Layers"), 0, False, 0) self._progress.show() objectIdMap = {} new_node = SceneNode() ## Put all nodes in a dict identified by ID for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if hasattr(node.getMeshData(), "layerData"): self._scene.getRoot().removeChild(node) else: objectIdMap[id(node)] = node settings = Application.getInstance().getActiveMachine() layerHeight = settings.getSettingValueByKey("layer_height") center = None if not settings.getSettingValueByKey("machine_center_is_zero"): center = numpy.array([settings.getSettingValueByKey("machine_width") / 2, 0.0, -settings.getSettingValueByKey("machine_depth") / 2]) else: center = numpy.array([0.0, 0.0, 0.0]) if self._progress: self._progress.setProgress(2) mesh = MeshData() for object in self._message.objects: try: node = objectIdMap[object.id] except KeyError: continue layerData = LayerData.LayerData() for layer in object.layers: layerData.addLayer(layer.id) layerData.setLayerHeight(layer.id, layer.height) layerData.setLayerThickness(layer.id, layer.thickness) for polygon in layer.polygons: points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. points = numpy.asarray(points, dtype=numpy.float32) points /= 1000 points = numpy.insert(points, 1, (layer.height / 1000), axis = 1) points[:,2] *= -1 points -= numpy.array(center) layerData.addPolygon(layer.id, polygon.type, points, polygon.line_width) if self._progress: self._progress.setProgress(50) # We are done processing all the layers we got from the engine, now create a mesh out of the data layerData.build() mesh.layerData = layerData if self._progress: self._progress.setProgress(100) new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide()
def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return mesh = MeshData() layer_data = LayerData.LayerData() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if (layer.id < min_layer_number): min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height new_points[:, 2] = -points[:, 1] new_points /= 1000 layer_data.addPolygon(abs_layer_number, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent( self._scene.getRoot()) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None
def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) object_id_map = {} new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return settings = Application.getInstance().getMachineManager( ).getWorkingProfile() mesh = MeshData() layer_data = LayerData.LayerData() layer_count = len(self._layers) current_layer = 0 for layer in self._layers: layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height new_points[:, 2] = -points[:, 1] new_points /= 1000 layer_data.addPolygon(layer.id, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent( self._scene.getRoot()) #Note: After this we can no longer abort! if not settings.getSettingValue("machine_center_is_zero"): new_node.setPosition( Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide()
def read(self, file_name): result = None extension = os.path.splitext(file_name)[1] if extension.lower() == self._supported_extension: result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, 'r') try: root = ET.parse(archive.open("3D/3dmodel.model")) # There can be multiple objects, try to load all of them. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) for object in objects: mesh = MeshData() node = SceneNode() vertex_list = [] #for vertex in object.mesh.vertices.vertex: for vertex in object.findall(".//3mf:vertex", self._namespaces): vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) triangles = object.findall(".//3mf:triangle", self._namespaces) mesh.reserveFaceCount(len(triangles)) #for triangle in object.mesh.triangles.triangle: for triangle in triangles: v1 = int(triangle.get("v1")) v2 = int(triangle.get("v2")) v3 = int(triangle.get("v3")) mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2]) #TODO: We currently do not check for normals and simply recalculate them. mesh.calculateNormals() node.setMeshData(mesh) node.setSelectable(True) transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces) if transformation: transformation = transformation[0] if transformation.get("transform"): splitted_transformation = transformation.get("transform").split() ## Transformation is saved as: ## M00 M01 M02 0.0 ## M10 M11 M12 0.0 ## M20 M21 M22 0.0 ## M30 M31 M32 1.0 ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() # Rotation & Scale temp_mat._data[0,0] = splitted_transformation[0] temp_mat._data[1,0] = splitted_transformation[1] temp_mat._data[2,0] = splitted_transformation[2] temp_mat._data[0,1] = splitted_transformation[3] temp_mat._data[1,1] = splitted_transformation[4] temp_mat._data[2,1] = splitted_transformation[5] temp_mat._data[0,2] = splitted_transformation[6] temp_mat._data[1,2] = splitted_transformation[7] temp_mat._data[2,2] = splitted_transformation[8] # Translation temp_mat._data[0,3] = splitted_transformation[9] temp_mat._data[1,3] = splitted_transformation[10] temp_mat._data[2,3] = splitted_transformation[11] node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3))) temp_quaternion = Quaternion() temp_quaternion.setByMatrix(temp_mat) node.setOrientation(temp_quaternion) # Magical scale extraction S2 = temp_mat.getTransposed().multiply(temp_mat) scale_x = math.sqrt(S2.at(0,0)) scale_y = math.sqrt(S2.at(1,1)) scale_z = math.sqrt(S2.at(2,2)) node.setScale(Vector(scale_x,scale_y,scale_z)) # We use a different coordinate frame, so rotate. rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0)) node.rotate(rotation) result.addChild(node) #If there is more then one object, group them. try: if len(objects) > 1: group_decorator = GroupDecorator() result.addDecorator(group_decorator) except: pass except Exception as e: Logger.log("e" ,"exception occured in 3mf reader: %s" , e) return result
# Copyright (c) 2015 Ultimaker B.V. # Uranium is released under the terms of the AGPLv3 or higher. from UM.Mesh.MeshData import MeshData @profile def getByteArray(mesh): return mesh.getVerticesAsByteArray() mesh = MeshData() mesh.reserveVertexCount(10000) for i in range(10000): mesh.addVertex(0, 1, 0) for i in range(100): a = getByteArray(mesh)
class DXFReader(MeshReader): def __init__(self): super(DXFReader, self).__init__() self._supported_extensions = [".dxf"] self._dxf = None self._mesh = None def read(self, file_name): self._dxf = DXFObjectReader.DXFObjectReader(open(file_name, "rt")) self._mesh = MeshData() for obj in self._dxf: if obj.getName() == "SECTION": if obj.get(2) == "ENTITIES": self._handleEntities() elif obj.get(2) == "TABLES": self._handleTables() else: Logger.log("d", "DXF: Got unknown section: %s", obj.get(2)) for obj in self._dxf: if obj.getName() == "ENDSEC": break else: Logger.log("d", "DXF: %s", obj) elif obj.getName() == "EOF": pass else: Logger.log("e", "DXF: Unexpected object: %s", obj) #self._mesh.calculateNormals() node = SceneNode() node.setMeshData(self._mesh) return node def _handleTables(self): for obj in self._dxf: if obj.getName() == "ENDSEC": break elif obj.getName() == "TABLE": if obj.get(2) == "": pass else: Logger.log("d", "DXF: Got unknown table: %s", obj.get(2)) for obj in self._dxf: if obj.getName() == "ENDTAB": break else: Logger.log("d", "DXF: %s", obj) else: Logger.log("e", "DXF: Unexpected object in tables section: %s", obj) def _handleEntities(self): for obj in self._dxf: if obj.getName() == "ENDSEC": break elif obj.getName() == "LINE": self._addLine(obj.get(10), obj.get(20), obj.get(11), obj.get(21)) elif obj.getName() == "LWPOLYLINE": for n in range(0, obj.count(20) - 1): self._addLine(obj.get(10, n), obj.get(20, n), obj.get(10, n + 1), obj.get(20, n + 1)) elif obj.getName() == "ARC": cx = float(obj.get(10)) cy = float(obj.get(20)) r = float(obj.get(40)) a_start = float(obj.get(50)) a_end = float(obj.get(51)) if a_end < a_start: a_end += 360.0 steps = math.ceil(((2.0 * math.pi * r) * (a_end - a_start) / 360.0) / 0.5) for n in range(0, steps): a0 = a_start + (a_end - a_start) * n / steps a1 = a_start + (a_end - a_start) * (n + 1) / steps self._addLine( cx + math.cos(math.radians(a0)) * r, cy + math.sin(math.radians(a0)) * r, cx + math.cos(math.radians(a1)) * r, cy + math.sin(math.radians(a1)) * r ) elif obj.getName() == "CIRCLE": cx = float(obj.get(10)) cy = float(obj.get(20)) r = float(obj.get(40)) steps = math.ceil((2.0 * math.pi * r) / 0.5) for n in range(0, steps): a0 = 360.0 * n / steps a1 = 360.0 * (n + 1) / steps self._addLine( cx + math.cos(math.radians(a0)) * r, cy + math.sin(math.radians(a0)) * r, cx + math.cos(math.radians(a1)) * r, cy + math.sin(math.radians(a1)) * r ) elif obj.getName() == "ELLIPSE": cx = float(obj.get(10)) cy = float(obj.get(20)) emx = float(obj.get(11)) emy = float(obj.get(21)) r_major = math.sqrt(emx * emx + emy * emy) r_minor = r_major * float(obj.get(40)) a_start = math.degrees(float(obj.get(41))) a_end = math.degrees(float(obj.get(42))) if a_end < a_start: a_end += 360.0 steps = math.ceil(((2.0 * math.pi * r) * (a_end - a_start) / 360.0) / 0.5) for n in range(0, steps): a0 = a_start + (a_end - a_start) * n / steps a1 = a_start + (a_end - a_start) * (n + 1) / steps self._addLine( cx + math.cos(math.radians(a0)) * r_major, cy + math.sin(math.radians(a0)) * r_minor, cx + math.cos(math.radians(a1)) * r_major, cy + math.sin(math.radians(a1)) * r_minor ) elif obj.getName() == "SPLINE": nurbs = NURBS.NURBS(int(obj.get(71))) for n in range(0, obj.count(40)): nurbs.addKnot(float(obj.get(40, n))) for n in range(0, obj.count(10)): nurbs.addPoint(float(obj.get(10, n)), float(obj.get(20, n))) points = nurbs.calculate(2) distance = math.sqrt((points[0][0] - points[-1][0]) * (points[0][0] - points[-1][0]) + (points[0][1] - points[-1][1]) * (points[0][1] - points[-1][1])) if distance < 1.0: point_count = int(max(2, distance / 0.1)) elif distance < 5.0: point_count = int(max(2, distance / 0.3)) else: point_count = int(max(2, distance / 0.5)) points = nurbs.calculate(point_count) for n in range(0, len(points) - 1): self._addLine(points[n][0], points[n][1], points[n + 1][0], points[n + 1][1]) else: Logger.log("w", "DXF: Unknown entity: %s", str(obj)) def _addLine(self, x0, y0, x1, y1): x0 = float(x0) x1 = float(x1) y0 = -float(y0) y1 = -float(y1) if x0 == x1 and y0 == y1: return self._mesh.addVertex(x0, 0, y0) self._mesh.addVertex(x1, 0, y1) self._mesh.addVertex(x1, 1, y1) self._mesh.addVertex(x0, 1, y0) self._mesh.addVertex(x1, 1, y1) self._mesh.addVertex(x0, 0, y0)
class MeshBuilder: def __init__(self): self._mesh_data = MeshData() def getData(self): return self._mesh_data def addLine(self, v0, v1, **kwargs): self._mesh_data.addVertex(v0.x, v0.y, v0.z) self._mesh_data.addVertex(v1.x, v1.y, v1.z) color = kwargs.get("color", None) if color: self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) def addFace(self, v0, v1, v2, **kwargs): normal = kwargs.get("normal", None) if normal: self._mesh_data.addFaceWithNormals( v0.x, v0.y, v0.z, normal.x, normal.y, normal.z, v1.x, v1.y, v1.z, normal.x, normal.y, normal.z, v2.x, v2.y, v2.z, normal.x, normal.y, normal.z ) else: self._mesh_data.addFace(v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z) color = kwargs.get("color", None) if color: self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 3, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) def addQuad(self, v0, v1, v2, v3, **kwargs): self.addFace(v0, v2, v1, color = kwargs.get("color"), normal = kwargs.get("normal") ) self.addFace(v0, v3, v2, color = kwargs.get("color"), normal = kwargs.get("normal") ) def addCube(self, **kwargs): width = kwargs["width"] height = kwargs["height"] depth = kwargs["depth"] center = kwargs.get("center", Vector(0, 0, 0)) minW = -width / 2 + center.x maxW = width / 2 + center.x minH = -height / 2 + center.y maxH = height / 2 + center.y minD = -depth / 2 + center.z maxD = depth / 2 + center.z start = self._mesh_data.getVertexCount() verts = numpy.asarray([ [minW, minH, maxD], [minW, maxH, maxD], [maxW, maxH, maxD], [maxW, minH, maxD], [minW, minH, minD], [minW, maxH, minD], [maxW, maxH, minD], [maxW, minH, minD], ], dtype=numpy.float32) self._mesh_data.addVertices(verts) indices = numpy.asarray([ [start, start + 2, start + 1], [start, start + 3, start + 2], [start + 3, start + 7, start + 6], [start + 3, start + 6, start + 2], [start + 7, start + 5, start + 6], [start + 7, start + 4, start + 5], [start + 4, start + 1, start + 5], [start + 4, start + 0, start + 1], [start + 1, start + 6, start + 5], [start + 1, start + 2, start + 6], [start + 0, start + 7, start + 3], [start + 0, start + 4, start + 7] ], 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, 9): self._mesh_data.setVertexColor(vertex_count - i, color) def addArc(self, **kwargs): radius = kwargs["radius"] axis = kwargs["axis"] max_angle = kwargs.get("angle", math.pi * 2) center = kwargs.get("center", Vector(0, 0, 0)) sections = kwargs.get("sections", 32) color = kwargs.get("color", None) if axis == Vector.Unit_Y: start = axis.cross(Vector.Unit_X).normalize() * radius else: start = axis.cross(Vector.Unit_Y).normalize() * radius angle_increment = max_angle / sections angle = 0 point = start + center m = Matrix() while angle <= max_angle: self._mesh_data.addVertex(point.x, point.y, point.z) angle += angle_increment m.setByRotationAxis(angle, axis) point = start.multiply(m) + center self._mesh_data.addVertex(point.x, point.y, point.z) if color: self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 2, color) self._mesh_data.setVertexColor(self._mesh_data.getVertexCount() - 1, color) 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)) 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)
class SceneNode(): class TransformSpace: Local = 1 Parent = 2 World = 3 ## Construct a scene node. # \param parent The parent of this node (if any). Only a root node should have None as a parent. # \param kwargs Keyword arguments. # Possible keywords: # - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True # - name \type{string} Name of the SceneNode. Defaults to empty string. def __init__(self, parent=None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] self._mesh_data = None # Local transformation (from parent to local) self._transformation = Matrix() # Convenience "components" of the transformation self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._shear = Vector(0.0, 0.0, 0.0) self._orientation = Quaternion() # World transformation (from root to local) self._world_transformation = Matrix() # Convenience "components" of the world_transformation self._derived_position = Vector() self._derived_orientation = Quaternion() self._derived_scale = Vector() self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._original_aabb = None self._aabb_job = None self._visible = kwargs.get("visible", True) self._name = kwargs.get("name", "") self._decorators = [] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy ## Set the center position of this node. # This is used to modify it's mesh data (and it's children) in such a way that they are centered. # In most cases this means that we use the center of mass as center (which most objects don't use) def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent ## Get the MeshData of the bounding box # \returns \type{MeshData} Bounding box mesh. def getBoundingBoxMesh(self): return self._bounding_box_mesh ## (re)Calculate the bounding box mesh. def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back else: self._resetAABB() ## Handler for the ParentChanged signal # \param node Node from which this event was triggered. def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) ## Signal for when a \type{SceneNodeDecorator} is added / removed. decoratorsChanged = Signal() ## Add a SceneNodeDecorator to this SceneNode. # \param \type{SceneNodeDecorator} decorator The decorator to add. # TODO: GetDecorator seems to imply that a scne node can only have a single decorator of a type, but we never enforce this. def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) ## Get all SceneNodeDecorators that decorate this SceneNode. # \return list of all SceneNodeDecorators. def getDecorators(self): return self._decorators ## Get SceneNodeDecorators by type. # \param dec_type type of decorator to return. def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator ## Remove all decorators def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) ## Remove decorator by type. # \param dec_type type of the decorator to remove. def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break ## Call a decoration of this SceneNode. # SceneNodeDecorators add Decorations, which are callable functions. # \param \type{string} function The function to be called. # \param *args # \param **kwargs def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None ## Does this SceneNode have a certain Decoration (as defined by a Decorator) # \param \type{string} function the function to check for. def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root? # \return |tupe{int} Steps from root (0 means it -is- the root). def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide whether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible ## Set the visibility of this SceneNode. def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect( self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation): self._transformation = transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) def getWorldOrientation(self): return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space=TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation, transform_space=TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.Local: orientation_matrix = orientation.toMatrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) def getWorldScale(self): return deepcopy(self._derived_scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space=TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale, transform_space=TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space=TransformSpace.Local): if not self._enabled: return translation_matrix = Matrix() translation_matrix.setByTranslation(translation) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position, transform_space=TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up=Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Get the bounding box of this node and its children. Without taking any transformation into account def getOriginalBoundingBox(self): if self._original_aabb: return self._original_aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation = self._transformation.decompose( ) self._position = translation self._scale = scale self._shear = shear orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation, copy=True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose( ) self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose( ) def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
class LayerView(View): def __init__(self): super().__init__() self._material = None self._num_layers = 0 self._layer_percentage = 0 # what percentage of layers need to be shown (SLider gives value between 0 - 100) self._proxy = LayerViewProxy.LayerViewProxy() self._controller.getScene().sceneChanged.connect(self._onSceneChanged) self._max_layers = 10 self._current_layer_num = 10 self._current_layer_mesh = None self._activity = False self._solid_layers = 5 def getActivity(self): return self._activity def getCurrentLayer(self): return self._current_layer_num def _onSceneChanged(self, node): self.calculateMaxLayers() def getMaxLayers(self): return self._max_layers def resetLayerData(self): self._current_layer_mesh = None def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() renderer.setRenderSelection(False) if not self._material: self._material = renderer.createMaterial(Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "vertexcolor.frag")) self._material.setUniformValue("u_color", [1.0, 0.0, 0.0, 1.0]) self._selection_material = renderer.createMaterial(Resources.getPath(Resources.Shaders, "basic.vert"), Resources.getPath(Resources.Shaders, "color.frag")) self._selection_material.setUniformValue("u_color", Color(35, 35, 35, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, material = self._selection_material, transparent = True) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh = layer_data, material = self._material, mode = Renderer.RenderLines, start = start, end = end) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue layer_mesh = layer_data.getLayer(layer).createMesh() if not layer_mesh or layer_mesh.getVertices() is None: continue self._current_layer_mesh.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_mesh.addColors(layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh = self._current_layer_mesh, material = self._material) def setLayer(self, value): if self._current_layer_num != value: self._current_layer_num = value if self._current_layer_num < 0: self._current_layer_num = 0 if self._current_layer_num > self._max_layers: self._current_layer_num = self._max_layers self._current_layer_mesh = None self.currentLayerNumChanged.emit() currentLayerNumChanged = Signal() def calculateMaxLayers(self): scene = self.getController().getScene() renderer = self.getRenderer() if renderer and self._material: self._activity = True renderer.setRenderSelection(False) self._old_max_layers = self._max_layers ## Recalculate num max layers new_max_layers = 0 for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): if node.getMeshData() and node.isVisible(): layer_data = node.callDecoration("getLayerData") if not layer_data: continue if new_max_layers < len(layer_data.getLayers()): new_max_layers = len(layer_data.getLayers()) - 1 if new_max_layers > 0 and new_max_layers != self._old_max_layers: self._max_layers = new_max_layers self.maxLayersChanged.emit() self._current_layer_num = self._max_layers # This makes sure we update the current layer self.setLayer(int(self._max_layers)) self.currentLayerNumChanged.emit() maxLayersChanged = Signal() currentLayerNumChanged = Signal() ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): return self._proxy def endRendering(self): pass def event(self, event): if event.type == Event.KeyPressEvent: if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + 1) if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - 1)
def __init__(self): self._mesh_data = MeshData()
def test_setMeshData(camera): with pytest.raises(AssertionError): camera.setMeshData(MeshData()) # This is allowed camera.setMeshData(None)
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent=None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect( self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space=TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._orientation = self._orientation * rotation elif transform_space == SceneNode.TransformSpace.Parent: self._orientation = rotation * self._orientation elif transform_space == SceneNode.TransformSpace.World: self._orientation = self._orientation * self._getDerivedOrientation( ).getInverse() * rotation * self._getDerivedOrientation() else: raise ValueError( "Unknown transform space {0}".format(transform_space)) self._orientation.normalize() self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. def setOrientation(self, orientation): if not self._enabled or orientation == self._orientation: return self._orientation = orientation self._orientation.normalize() self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space=TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._scale = self._scale.scale(scale) elif transform_space == SceneNode.TransformSpace.Parent: raise NotImplementedError() elif transform_space == SceneNode.TransformSpace.World: if self._parent: scale_change = Vector(1, 1, 1) - scale if scale_change.x < 0 or scale_change.y < 0 or scale_change.z < 0: direction = -1 else: direction = 1 # Hackish way to do this, but this seems to correctly scale the object. change_vector = self._scale.scale( self._getDerivedOrientation().getInverse().rotate( scale_change)) if change_vector.x < 0 and direction == 1: change_vector.setX(-change_vector.x) if change_vector.x > 0 and direction == -1: change_vector.setX(-change_vector.x) if change_vector.y < 0 and direction == 1: change_vector.setY(-change_vector.y) if change_vector.y > 0 and direction == -1: change_vector.setY(-change_vector.y) if change_vector.z < 0 and direction == 1: change_vector.setZ(-change_vector.z) if change_vector.z > 0 and direction == -1: change_vector.setZ(-change_vector.z) self._scale -= self._scale.scale(change_vector) else: raise ValueError( "Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. def setScale(self, scale): if not self._enabled or scale == self._scale: return self._scale = scale self._transformChanged() ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): if not self._derived_position: self._updateTransformation() return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space=TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._position += self._orientation.rotate(translation) elif transform_space == SceneNode.TransformSpace.Parent: self._position += translation elif transform_space == SceneNode.TransformSpace.World: if self._parent: self._position += (1.0 / self._parent._getDerivedScale()).scale( self._parent._getDerivedOrientation( ).getInverse().rotate(translation)) else: self._position += translation self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. def setPosition(self, position): if not self._enabled or position == self._position: return self._position = position self._transformChanged() ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up=Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) if self._parent: self._orientation = self._parent._getDerivedOrientation( ) * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged() ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _getDerivedPosition(self): if not self._derived_position: self._updateTransformation() return self._derived_position def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() return self._derived_orientation def _getDerivedScale(self): if not self._derived_scale: self._updateTransformation() return self._derived_scale def _transformChanged(self): self._resetAABB() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): self._transformation = Matrix.fromPositionOrientationScale( self._position, self._orientation, self._scale) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(self._scale) else: self._derived_scale = self._scale self._derived_position = parent_orientation.rotate( parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale( self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = self._scale self._world_transformation = self._transformation def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
def getMeshDataTransformed(self) -> Optional[MeshData]: return MeshData(vertices=self.getMeshDataTransformedVertices())
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): mesh = None scene_node = None scene_node = SceneNode() mesh = MeshData() scene_node.setMeshData(mesh) img = QImage(file_name) if img.isNull(): Logger.log("e", "Image is corrupt.") return None width = max(img.width(), 2) height = max(img.height(), 2) aspect = height / width if img.width() < 2 or img.height() < 2: img = img.scaled(width, height, Qt.IgnoreAspectRatio) base_height = max(base_height, 0) peak_height = max(peak_height, -base_height) xz_size = max(xz_size, 1) scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: scale_vector.setZ(scale_vector.z * aspect) elif height > width: scale_vector.setX(scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width if height > width: scale_factor = max_size / height width = int(max(round(width * scale_factor), 2)) height = int(max(round(height * scale_factor), 2)) img = img.scaled(width, height, Qt.IgnoreAspectRatio) width_minus_one = width - 1 height_minus_one = height - 1 Job.yieldThread() texel_width = 1.0 / (width_minus_one) * scale_vector.x texel_height = 1.0 / (height_minus_one) * scale_vector.z height_data = numpy.zeros((height, width), dtype=numpy.float32) for x in range(0, width): for y in range(0, height): qrgb = img.pixel(x, y) avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255) height_data[y, x] = avg Job.yieldThread() if image_color_invert: height_data = 1 - height_data for i in range(0, blur_iterations): copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode="edge") height_data += copy[1:-1, 2:] height_data += copy[1:-1, :-2] height_data += copy[2:, 1:-1] height_data += copy[:-2, 1:-1] height_data += copy[2:, 2:] height_data += copy[:-2, 2:] height_data += copy[2:, :-2] height_data += copy[:-2, :-2] height_data /= 9 Job.yieldThread() height_data *= scale_vector.y height_data += base_height heightmap_face_count = 2 * height_minus_one * width_minus_one total_face_count = heightmap_face_count + (width_minus_one * 2) * ( height_minus_one * 2) + 2 mesh.reserveFaceCount(total_face_count) # initialize to texel space vertex offsets. # 6 is for 6 vertices for each texel quad. heightmap_vertices = numpy.zeros( (width_minus_one * height_minus_one, 6, 3), dtype=numpy.float32) heightmap_vertices = heightmap_vertices + numpy.array( [[[0, base_height, 0], [0, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, texel_height], [texel_width, base_height, 0], [0, base_height, 0]]], dtype=numpy.float32) offsetsz, offsetsx = numpy.mgrid[0:height_minus_one, 0:width - 1] offsetsx = numpy.array(offsetsx, numpy.float32).reshape( -1, 1) * texel_width offsetsz = numpy.array(offsetsz, numpy.float32).reshape( -1, 1) * texel_height # offsets for each texel quad heightmap_vertex_offsets = numpy.concatenate([ offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz ], 1) heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape( -1, 6, 3) # apply height data to y values heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, : -1].reshape( -1) heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1) heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[ 1:, 1:].reshape(-1) heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1) heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3) mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3) mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices mesh._vertex_count = heightmap_vertices.size // 3 mesh._face_count = heightmap_indices.size // 3 geo_width = width_minus_one * texel_width geo_height = height_minus_one * texel_height # bottom mesh.addFace(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) mesh.addFace(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) # north and south walls for n in range(0, width_minus_one): x = n * texel_width nx = (n + 1) * texel_width hn0 = height_data[0, n] hn1 = height_data[0, n + 1] hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] mesh.addFace(x, 0, 0, nx, 0, 0, nx, hn1, 0) mesh.addFace(nx, hn1, 0, x, hn0, 0, x, 0, 0) mesh.addFace(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) mesh.addFace(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) # west and east walls for n in range(0, height_minus_one): y = n * texel_height ny = (n + 1) * texel_height hw0 = height_data[n, 0] hw1 = height_data[n + 1, 0] he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] mesh.addFace(0, 0, y, 0, 0, ny, 0, hw1, ny) mesh.addFace(0, hw1, ny, 0, hw0, y, 0, 0, y) mesh.addFace(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) mesh.addFace(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) mesh.calculateNormals(fast=True) return scene_node
class PathResultDecorator(SceneNodeDecorator): _move_color = Color(0, 0, 0, 128) _cut_color = Color(255, 0, 0, 255) _engrave_color = Color(0, 0, 255, 255) def __init__(self): super().__init__() self._paths = None def getCutPaths(self): return self._cut_paths def getEngravePaths(self): return self._engrave_paths def setPaths(self, engrave_paths, cut_paths): self._cut_paths = cut_paths self._engrave_paths = engrave_paths self._mesh = MeshData() last_point = None for path in engrave_paths.closed_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._engrave_color) last_point = point self._addMeshLine(last_point, path[0], self._engrave_color) last_point = path[0] for path in engrave_paths.open_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._engrave_color) last_point = point for path in cut_paths.closed_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._cut_color) last_point = point self._addMeshLine(last_point, path[0], self._cut_color) last_point = path[0] for path in cut_paths.open_paths: if last_point is not None: self._addMeshLine(last_point, path[0], self._move_color) last_point = path[0] for point in path[1:]: self._addMeshLine(last_point, point, self._cut_color) last_point = point self.getNode().setMeshData(self._mesh) def _addMeshLine(self, p0, p1, color): self._mesh.addVertex(p0[0] / Paths.SCALE, 0.0, p0[1] / Paths.SCALE) self._mesh.addVertex(p1[0] / Paths.SCALE, 0.0, p1[1] / Paths.SCALE) self._mesh.setVertexColor(self._mesh.getVertexCount() - 2, color) self._mesh.setVertexColor(self._mesh.getVertexCount() - 1, color)
def read(self, file_name): mesh = None scene_node = None extension = os.path.splitext(file_name)[1] if extension.lower() in self._supported_extensions: vertex_list = [] normal_list = [] uv_list = [] face_list = [] scene_node = SceneNode() mesh = MeshData() scene_node.setMeshData(mesh) f = open(file_name, "rt") for line in f: parts = line.split() if len(parts) < 1: continue if parts[0] == "v": vertex_list.append([float(parts[1]), float(parts[3]), -float(parts[2])]) if parts[0] == "vn": normal_list.append([float(parts[1]), float(parts[3]), -float(parts[2])]) if parts[0] == "vt": uv_list.append([float(parts[1]), float(parts[2])]) if parts[0] == "f": parts = [i for i in map(lambda p: p.split("/"), parts)] for idx in range(1, len(parts)-2): data = [int(parts[1][0]), int(parts[idx+1][0]), int(parts[idx+2][0])] if len(parts[1]) > 2: data += [int(parts[1][2]), int(parts[idx+1][2]), int(parts[idx+2][2])] if parts[1][1] and parts[idx+1][1] and parts[idx+2][1]: data += [int(parts[1][1]), int(parts[idx+1][1]), int(parts[idx+2][1])] face_list.append(data) Job.yieldThread() f.close() mesh.reserveVertexCount(3 * len(face_list)) num_vertices = len(vertex_list) num_normals = len(normal_list) for face in face_list: # Substract 1 from index, as obj starts counting at 1 instead of 0 i = face[0] - 1 j = face[1] - 1 k = face[2] - 1 if len(face) > 3: ni = face[3] - 1 nj = face[4] - 1 nk = face[5] - 1 else: ni = -1 nj = -1 nk = -1 if len(face) > 6: ui = face[6] - 1 uj = face[7] - 1 uk = face[8] - 1 else: ui = -1 uj = -1 uk = -1 #TODO: improve this handling, this can cause weird errors (negative indexes are relative indexes, and are not properly handled) if i < 0 or i >= num_vertices: i = 0 if j < 0 or j >= num_vertices: j = 0 if k < 0 or k >= num_vertices: k = 0 if ni != -1 and nj != -1 and nk != -1: mesh.addFaceWithNormals(vertex_list[i][0], vertex_list[i][1], vertex_list[i][2], normal_list[ni][0], normal_list[ni][1], normal_list[ni][2], vertex_list[j][0], vertex_list[j][1], vertex_list[j][2], normal_list[nj][0], normal_list[nj][1], normal_list[nj][2], vertex_list[k][0], vertex_list[k][1], vertex_list[k][2],normal_list[nk][0], normal_list[nk][1], normal_list[nk][2]) else: mesh.addFace(vertex_list[i][0], vertex_list[i][1], vertex_list[i][2], vertex_list[j][0], vertex_list[j][1], vertex_list[j][2], vertex_list[k][0], vertex_list[k][1], vertex_list[k][2]) if ui != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 3, uv_list[ui][0], uv_list[ui][1]) if uj != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 2, uv_list[uj][0], uv_list[uj][1]) if uk != -1: mesh.setVertexUVCoordinates(mesh.getVertexCount() - 1, uv_list[uk][0], uv_list[uk][1]) Job.yieldThread() if not mesh.hasNormals(): mesh.calculateNormals(fast = True) return scene_node
def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._selection_shader: self._selection_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader")) self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, transparent = True, shader = self._selection_shader) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh = layer_data, mode = RenderBatch.RenderMode.Lines, range = (start, end)) # We currently recreate the current "solid" layers every time a if not self._current_layer_mesh: self._current_layer_mesh = MeshData() for i in range(self._solid_layers): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer(layer).createMesh() if not layer_mesh or layer_mesh.getVertices() is None: continue except: continue if self._current_layer_mesh: #Threading thing; Switching between views can cause the current layer mesh to be deleted. self._current_layer_mesh.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 if self._current_layer_mesh: self._current_layer_mesh.addColors(layer_mesh.getColors() * brightness) if self._current_layer_mesh: renderer.queueNode(node, mesh = self._current_layer_mesh) if not self._current_layer_jumps: self._current_layer_jumps = MeshData() for i in range(1): layer = self._current_layer_num - i if layer < 0: continue try: layer_mesh = layer_data.getLayer(layer).createJumps() if not layer_mesh or layer_mesh.getVertices() is None: continue except: continue self._current_layer_jumps.addVertices(layer_mesh.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. brightness = (2.0 - (i / self._solid_layers)) / 2.0 self._current_layer_jumps.addColors(layer_mesh.getColors() * brightness) renderer.queueNode(node, mesh = self._current_layer_jumps)
def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) objectIdMap = {} new_node = SceneNode() ## Put all nodes in a dict identified by ID for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) else: objectIdMap[id(node)] = node Job.yieldThread() settings = Application.getInstance().getMachineManager( ).getActiveProfile() layerHeight = settings.getSettingValue("layer_height") center = None if not settings.getSettingValue("machine_center_is_zero"): center = numpy.array([ settings.getSettingValue("machine_width") / 2, 0.0, -settings.getSettingValue("machine_depth") / 2 ]) else: center = numpy.array([0.0, 0.0, 0.0]) mesh = MeshData() layer_data = LayerData.LayerData() layer_count = 0 for object in self._message.objects: layer_count += len(object.layers) current_layer = 0 for object in self._message.objects: try: node = objectIdMap[object.id] except KeyError: continue for layer in object.layers: layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for polygon in layer.polygons: points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. points = numpy.asarray(points, dtype=numpy.float32) points /= 1000 points = numpy.insert(points, 1, (layer.height / 1000), axis=1) points[:, 2] *= -1 points -= center layer_data.addPolygon(layer.id, polygon.type, points, polygon.line_width) Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide()
def __init__(self, parent = None): super().__init__(parent) lines = MeshData() lines.addVertex(0, 0, 0) lines.addVertex(0, 20, 0) lines.addVertex(0, 0, 0) lines.addVertex(20, 0, 0) lines.addVertex(0, 0, 0) lines.addVertex(0, 0, 20) lines.setVertexColor(0, ToolHandle.YAxisColor) lines.setVertexColor(1, ToolHandle.YAxisColor) lines.setVertexColor(2, ToolHandle.XAxisColor) lines.setVertexColor(3, ToolHandle.XAxisColor) lines.setVertexColor(4, ToolHandle.ZAxisColor) lines.setVertexColor(5, ToolHandle.ZAxisColor) self.setLineMesh(lines) mb = MeshBuilder() mb.addCube( width = 2, height = 2, depth = 2, center = Vector(0, 0, 0), color = ToolHandle.AllAxisColor ) mb.addCube( width = 2, height = 2, depth = 2, center = Vector(0, 20, 0), color = ToolHandle.YAxisColor ) mb.addCube( width = 2, height = 2, depth = 2, center = Vector(20, 0, 0), color = ToolHandle.XAxisColor ) mb.addCube( width = 2, height = 2, depth = 2, center = Vector(0, 0, 20), color = ToolHandle.ZAxisColor ) self.setSolidMesh(mb.getData()) mb = MeshBuilder() mb.addCube( width = 4, height = 20, depth = 4, center = Vector(0, 10, 0), color = ToolHandle.YAxisColor ) mb.addCube( width = 4, height = 4, depth = 4, center = Vector(0, 20, 0), color = ToolHandle.YAxisColor ) mb.addCube( width = 20, height = 4, depth = 4, center = Vector(10, 0, 0), color = ToolHandle.XAxisColor ) mb.addCube( width = 4, height = 4, depth = 4, center = Vector(20, 0, 0), color = ToolHandle.XAxisColor ) mb.addCube( width = 4, height = 4, depth = 20, center = Vector(0, 0, 10), color = ToolHandle.ZAxisColor ) mb.addCube( width = 4, height = 4, depth = 4, center = Vector(0, 0, 20), color = ToolHandle.ZAxisColor ) mb.addCube( width = 8, height = 8, depth = 8, center = Vector(0, 0, 0), color = ToolHandle.AllAxisColor ) self.setSelectionMesh(mb.getData())