class MeshData: def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type = MeshType.faces, attributes=None): self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) self._indices = NumPyUtil.immutableNDArray(indices) self._colors = NumPyUtil.immutableNDArray(colors) self._uvs = NumPyUtil.immutableNDArray(uvs) self._vertex_count = len(self._vertices) if self._vertices is not None else 0 self._face_count = len(self._indices) if self._indices is not None else 0 self._type = type self._file_name = file_name # type: str # original center position self._center_position = center_position # original zero position, is changed after transformation if zero_position is not None: self._zero_position = zero_position else: self._zero_position = Vector(0, 0, 0) # type: Vector self._convex_hull = None # type: Optional[scipy.spatial.ConvexHull] self._convex_hull_vertices = None # type: Optional[numpy.ndarray] self._convex_hull_lock = threading.Lock() self._attributes = {} if attributes is not None: for key, attribute in attributes.items(): new_value = {} for attribute_key, attribute_value in attribute.items(): if attribute_key == "value": new_value["value"] = NumPyUtil.immutableNDArray(attribute_value) else: new_value[attribute_key] = attribute_value self._attributes[key] = new_value ## Create a new MeshData with specified changes # \return \type{MeshData} def set(self, vertices=Reuse, normals=Reuse, indices=Reuse, colors=Reuse, uvs=Reuse, file_name=Reuse, center_position=Reuse, zero_position=Reuse, attributes=Reuse) -> "MeshData": vertices = vertices if vertices is not Reuse else self._vertices normals = normals if normals is not Reuse else self._normals indices = indices if indices is not Reuse else self._indices colors = colors if colors is not Reuse else self._colors uvs = uvs if uvs is not Reuse else self._uvs file_name = file_name if file_name is not Reuse else self._file_name center_position = center_position if center_position is not Reuse else self._center_position zero_position = zero_position if zero_position is not Reuse else self._zero_position attributes = attributes if attributes is not Reuse else self._attributes return MeshData(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, file_name=file_name, center_position=center_position, zero_position=zero_position, attributes=attributes) def getHash(self): m = hashlib.sha256() m.update(self.getVerticesAsByteArray()) return m.hexdigest() def getCenterPosition(self) -> Vector: return self._center_position def getZeroPosition(self) -> Vector: return self._zero_position def getType(self): return self._type def getFaceCount(self) -> int: return self._face_count ## Get the array of vertices def getVertices(self) -> numpy.ndarray: return self._vertices ## Get the number of vertices def getVertexCount(self) -> int: return self._vertex_count ## Get a vertex by index def getVertex(self, index): try: return self._vertices[index] except IndexError: return None ## Return whether this mesh has vertex normals. def hasNormals(self) -> bool: return self._normals is not None ## Return the list of vertex normals. def getNormals(self) -> numpy.ndarray: return self._normals ## Return whether this mesh has indices. def hasIndices(self) -> bool: return self._indices is not None ## Get the array of indices # \return \type{numpy.ndarray} def getIndices(self) -> numpy.ndarray: return self._indices def hasColors(self) -> bool: return self._colors is not None def getColors(self) -> numpy.ndarray: return self._colors def hasUVCoordinates(self) -> bool: return self._uvs is not None def getFileName(self) -> str: return self._file_name ## Transform the meshdata, center and zero position by given Matrix # \param transformation 4x4 homogenous transformation matrix def getTransformed(self, transformation: Matrix) -> "MeshData": if self._vertices is not None: transformed_vertices = transformVertices(self._vertices, transformation) transformed_normals = transformNormals(self._normals, transformation) if self._normals is not None else None transformation_matrix = transformation.getTransposed() if self._center_position is not None: center_position = self._center_position.multiply(transformation_matrix) else: center_position = Reuse zero_position = self._zero_position.multiply(transformation_matrix) return self.set(vertices=transformed_vertices, normals=transformed_normals, center_position=center_position, zero_position=zero_position) else: return MeshData(vertices = self._vertices) ## Get the extents of this mesh. # # \param matrix The transformation matrix from model to world coordinates. def getExtents(self, matrix: Optional[Matrix] = None) -> Optional[AxisAlignedBox]: if self._vertices is None: return None data = numpy.pad(self.getConvexHullVertices(), ((0, 0), (0, 1)), "constant", constant_values=(0.0, 1.0)) if matrix is not None: transposed = matrix.getTransposed().getData() data = data.dot(transposed) data += transposed[:, 3] data = data[:, 0:3] min = data.min(axis=0) max = data.max(axis=0) return AxisAlignedBox(minimum=Vector(min[0], min[1], min[2]), maximum=Vector(max[0], max[1], max[2])) ## Get all vertices of this mesh as a bytearray # # \return A bytearray object with 3 floats per vertex. def getVerticesAsByteArray(self) -> Optional[bytes]: if self._vertices is None: return None return self._vertices.tostring() ## Get all normals of this mesh as a bytearray # # \return A bytearray object with 3 floats per normal. def getNormalsAsByteArray(self) -> Optional[bytes]: if self._normals is None: return None return self._normals.tostring() ## Get all indices as a bytearray # # \return A bytearray object with 3 ints per face. def getIndicesAsByteArray(self) -> Optional[bytes]: if self._indices is None: return None return self._indices.tostring() def getColorsAsByteArray(self) -> Optional[bytes]: if self._colors is None: return None return self._colors.tostring() def getUVCoordinatesAsByteArray(self) -> Optional[bytes]: if self._uvs is None: return None return self._uvs.tostring() ####################################################################### # Convex hull handling ####################################################################### def _computeConvexHull(self): points = self.getVertices() if points is None: return self._convex_hull = approximateConvexHull(points, MAXIMUM_HULL_VERTICES_COUNT) ## Gets the Convex Hull of this mesh # # \return \type{scipy.spatial.ConvexHull} def getConvexHull(self) -> Optional[scipy.spatial.ConvexHull]: with self._convex_hull_lock: if self._convex_hull is None: self._computeConvexHull() return self._convex_hull ## Gets the convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullVertices(self) -> numpy.ndarray: if self._convex_hull_vertices is None: convex_hull = self.getConvexHull() self._convex_hull_vertices = numpy.take(convex_hull.points, convex_hull.vertices, axis=0) return self._convex_hull_vertices ## Gets transformed convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullTransformedVertices(self, transformation: Matrix) -> Optional[numpy.ndarray]: vertices = self.getConvexHullVertices() if vertices is not None: return transformVertices(vertices, transformation) else: return None def hasAttribute(self, key: str) -> bool: return key in self._attributes ## the return value is a dict with at least keys opengl_name, opengl_type, value def getAttribute(self, key: str): return self._attributes[key] ## Return attribute names in alphabetical order # The sorting assures that the order is always the same. def attributeNames(self) -> List[str]: result = list(self._attributes.keys()) result.sort() return result def toString(self) -> str: return "MeshData(_vertices=" + str(self._vertices) + ", _normals=" + str(self._normals) + ", _indices=" + \ str(self._indices) + ", _colors=" + str(self._colors) + ", _uvs=" + str(self._uvs) + ", _attributes=" + \ str(self._attributes.keys()) + ") "
class MeshData: def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type=MeshType.faces, attributes=None): self._application = None # Initialize this later otherwise unit tests break self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) self._indices = NumPyUtil.immutableNDArray(indices) self._colors = NumPyUtil.immutableNDArray(colors) self._uvs = NumPyUtil.immutableNDArray(uvs) self._vertex_count = len( self._vertices) if self._vertices is not None else 0 self._face_count = len( self._indices) if self._indices is not None else 0 self._type = type self._file_name = file_name # type: Optional[str] # original center position self._center_position = center_position # original zero position, is changed after transformation if zero_position is not None: self._zero_position = zero_position else: self._zero_position = Vector(0, 0, 0) # type: Vector self._convex_hull = None # type: Optional[scipy.spatial.ConvexHull] self._convex_hull_vertices = None # type: Optional[numpy.ndarray] self._convex_hull_lock = threading.Lock() self._attributes = {} if attributes is not None: for key, attribute in attributes.items(): new_value = {} for attribute_key, attribute_value in attribute.items(): if attribute_key == "value": new_value["value"] = NumPyUtil.immutableNDArray( attribute_value) else: new_value[attribute_key] = attribute_value self._attributes[key] = new_value ## Triggered when this file is deleted. # # The file will then no longer be watched for changes. def __del__(self): if self._file_name: if self._application: self._application.getController().getScene().removeWatchedFile( self._file_name) ## Create a new MeshData with specified changes # \return \type{MeshData} def set(self, vertices=Reuse, normals=Reuse, indices=Reuse, colors=Reuse, uvs=Reuse, file_name=Reuse, center_position=Reuse, zero_position=Reuse, attributes=Reuse) -> "MeshData": vertices = vertices if vertices is not Reuse else self._vertices normals = normals if normals is not Reuse else self._normals indices = indices if indices is not Reuse else self._indices colors = colors if colors is not Reuse else self._colors uvs = uvs if uvs is not Reuse else self._uvs file_name = file_name if file_name is not Reuse else self._file_name center_position = center_position if center_position is not Reuse else self._center_position zero_position = zero_position if zero_position is not Reuse else self._zero_position attributes = attributes if attributes is not Reuse else self._attributes return MeshData(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, file_name=file_name, center_position=center_position, zero_position=zero_position, attributes=attributes) def getHash(self): m = hashlib.sha256() m.update(self.getVerticesAsByteArray()) return m.hexdigest() def getCenterPosition(self) -> Vector: return self._center_position def getZeroPosition(self) -> Vector: return self._zero_position def getType(self): return self._type def getFaceCount(self) -> int: return self._face_count ## Get the array of vertices def getVertices(self) -> numpy.ndarray: return self._vertices ## Get the number of vertices def getVertexCount(self) -> int: return self._vertex_count ## Get a vertex by index def getVertex(self, index): try: return self._vertices[index] except IndexError: return None ## Return whether this mesh has vertex normals. def hasNormals(self) -> bool: return self._normals is not None ## Return the list of vertex normals. def getNormals(self) -> numpy.ndarray: return self._normals ## Return whether this mesh has indices. def hasIndices(self) -> bool: return self._indices is not None ## Get the array of indices # \return \type{numpy.ndarray} def getIndices(self) -> numpy.ndarray: return self._indices def hasColors(self) -> bool: return self._colors is not None def getColors(self) -> numpy.ndarray: return self._colors def hasUVCoordinates(self) -> bool: return self._uvs is not None def getFileName(self) -> Optional[str]: return self._file_name ## Transform the meshdata, center and zero position by given Matrix # \param transformation 4x4 homogenous transformation matrix def getTransformed(self, transformation: Matrix) -> "MeshData": if self._vertices is not None: transformed_vertices = transformVertices(self._vertices, transformation) transformed_normals = transformNormals( self._normals, transformation) if self._normals is not None else None transformation_matrix = transformation.getTransposed() if self._center_position is not None: center_position = self._center_position.multiply( transformation_matrix) else: center_position = Reuse zero_position = self._zero_position.multiply(transformation_matrix) return self.set(vertices=transformed_vertices, normals=transformed_normals, center_position=center_position, zero_position=zero_position) else: return MeshData(vertices=self._vertices) ## Get the extents of this mesh. # # \param matrix The transformation matrix from model to world coordinates. def getExtents(self, matrix: Optional[Matrix] = None ) -> Optional[AxisAlignedBox]: if self._vertices is None: return None if matrix is not None: data = self.getConvexHullTransformedVertices(matrix) else: data = self.getConvexHullVertices() if data is None: return None min = data.min(axis=0) max = data.max(axis=0) return AxisAlignedBox(minimum=Vector(min[0], min[1], min[2]), maximum=Vector(max[0], max[1], max[2])) ## Get all vertices of this mesh as a bytearray # # \return A bytearray object with 3 floats per vertex. def getVerticesAsByteArray(self) -> Optional[bytes]: if self._vertices is None: return None return self._vertices.tostring() ## Get all normals of this mesh as a bytearray # # \return A bytearray object with 3 floats per normal. def getNormalsAsByteArray(self) -> Optional[bytes]: if self._normals is None: return None return self._normals.tostring() ## Get all indices as a bytearray # # \return A bytearray object with 3 ints per face. def getIndicesAsByteArray(self) -> Optional[bytes]: if self._indices is None: return None return self._indices.tostring() def getColorsAsByteArray(self) -> Optional[bytes]: if self._colors is None: return None return self._colors.tostring() def getUVCoordinatesAsByteArray(self) -> Optional[bytes]: if self._uvs is None: return None return self._uvs.tostring() ####################################################################### # Convex hull handling ####################################################################### def _computeConvexHull(self): points = self.getVertices() if points is None: return self._convex_hull = approximateConvexHull(points, MAXIMUM_HULL_VERTICES_COUNT) ## Gets the Convex Hull of this mesh # # \return \type{scipy.spatial.ConvexHull} def getConvexHull(self) -> Optional[scipy.spatial.ConvexHull]: with self._convex_hull_lock: if self._convex_hull is None: self._computeConvexHull() return self._convex_hull ## Gets the convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullVertices(self) -> Optional[numpy.ndarray]: if self._convex_hull_vertices is None: convex_hull = self.getConvexHull() if convex_hull is None: return None self._convex_hull_vertices = numpy.take(convex_hull.points, convex_hull.vertices, axis=0) return self._convex_hull_vertices ## Gets transformed convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullTransformedVertices( self, transformation: Matrix) -> Optional[numpy.ndarray]: vertices = self.getConvexHullVertices() if vertices is not None: return transformVertices(vertices, transformation) else: return None ## Gets the plane the supplied face lies in. The resultant plane is specified by a point and a normal. # # \param face_id \type{int} The index of the face (not the flattened indices). # \return \type{Tuple[numpy.ndarray, numpy.ndarray]} A plane, the 1st vector is the center, the 2nd the normal. def getFacePlane(self, face_id: int) -> Tuple[numpy.ndarray, numpy.ndarray]: if not self._indices or len(self._indices) == 0: base_index = face_id * 3 v_a = self._vertices[base_index] v_b = self._vertices[base_index + 1] v_c = self._vertices[base_index + 2] else: v_a = self._vertices[self._indices[face_id][0]] v_b = self._vertices[self._indices[face_id][1]] v_c = self._vertices[self._indices[face_id][2]] in_point = (v_a + v_b + v_c) / 3.0 face_normal = numpy.cross(v_b - v_a, v_c - v_a) return in_point, face_normal def hasAttribute(self, key: str) -> bool: return key in self._attributes ## the return value is a dict with at least keys opengl_name, opengl_type, value def getAttribute(self, key: str): return self._attributes[key] ## Return attribute names in alphabetical order # The sorting assures that the order is always the same. def attributeNames(self) -> List[str]: result = list(self._attributes.keys()) result.sort() return result def toString(self) -> str: return "MeshData(_vertices=" + str(self._vertices) + ", _normals=" + str(self._normals) + ", _indices=" + \ str(self._indices) + ", _colors=" + str(self._colors) + ", _uvs=" + str(self._uvs) + ", _attributes=" + \ str(self._attributes.keys()) + ") "
class MeshData: """Class to hold a list of verts and possibly how (and if) they are connected. This class stores three numpy arrays that contain the data for a mesh. Vertices are stored as a two-dimensional array of floats with the rows being individual vertices and the three columns being the X, Y and Z components of the vertices. Normals are stored in the same manner and kept in sync with the vertices. Indices are stored as a two-dimensional array of integers with the rows being the individual faces and the three columns being the indices that refer to the individual vertices. attributes: a dict with {"value", "opengl_type", "opengl_name"} type in vector2f, vector3f, uniforms, ... """ def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type=MeshType.faces, attributes=None) -> None: self._application = None # Initialize this later otherwise unit tests break self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) self._indices = NumPyUtil.immutableNDArray(indices) self._colors = NumPyUtil.immutableNDArray(colors) self._uvs = NumPyUtil.immutableNDArray(uvs) self._vertex_count = len( self._vertices) if self._vertices is not None else 0 self._face_count = len( self._indices) if self._indices is not None else 0 self._type = type self._file_name = file_name # type: Optional[str] # original center position self._center_position = center_position # original zero position, is changed after transformation if zero_position is not None: self._zero_position = zero_position else: self._zero_position = Vector(0, 0, 0) self._convex_hull = None # type: Optional[scipy.spatial.ConvexHull] self._convex_hull_vertices = None # type: Optional[numpy.ndarray] self._convex_hull_lock = threading.Lock() self._attributes = {} # type: Dict[str, Any] if attributes is not None: for key, attribute in attributes.items(): new_value = {} for attribute_key, attribute_value in attribute.items(): if attribute_key == "value": new_value["value"] = NumPyUtil.immutableNDArray( attribute_value) else: new_value[attribute_key] = attribute_value self._attributes[key] = new_value def __del__(self): """Triggered when this file is deleted. The file will then no longer be watched for changes. """ if self._file_name: if self._application: self._application.getController().getScene().removeWatchedFile( self._file_name) def set(self, vertices=Reuse, normals=Reuse, indices=Reuse, colors=Reuse, uvs=Reuse, file_name=Reuse, center_position=Reuse, zero_position=Reuse, attributes=Reuse) -> "MeshData": """Create a new MeshData with specified changes :return: :type{MeshData} """ vertices = vertices if vertices is not Reuse else self._vertices normals = normals if normals is not Reuse else self._normals indices = indices if indices is not Reuse else self._indices colors = colors if colors is not Reuse else self._colors uvs = uvs if uvs is not Reuse else self._uvs file_name = file_name if file_name is not Reuse else self._file_name center_position = center_position if center_position is not Reuse else self._center_position zero_position = zero_position if zero_position is not Reuse else self._zero_position attributes = attributes if attributes is not Reuse else self._attributes return MeshData(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, file_name=file_name, center_position=center_position, zero_position=zero_position, attributes=attributes) def getHash(self): m = hashlib.sha256() m.update(self.getVerticesAsByteArray()) return m.hexdigest() def getCenterPosition(self) -> Vector: return self._center_position def getZeroPosition(self) -> Vector: return self._zero_position def getType(self): return self._type def getFaceCount(self) -> int: return self._face_count def getVertices(self) -> numpy.ndarray: """Get the array of vertices""" return self._vertices def getVertexCount(self) -> int: """Get the number of vertices""" return self._vertex_count def getVertex(self, index): """Get a vertex by index""" try: return self._vertices[index] except IndexError: return None def hasNormals(self) -> bool: """Return whether this mesh has vertex normals.""" return self._normals is not None def getNormals(self) -> numpy.ndarray: """Return the list of vertex normals.""" return self._normals def hasIndices(self) -> bool: """Return whether this mesh has indices.""" return self._indices is not None def getIndices(self) -> numpy.ndarray: """Get the array of indices :return: :type{numpy.ndarray} """ if self._indices is not None and self._indices.dtype != "int32": self._indices = numpy.asarray(self._indices, dtype=numpy.int32) return self._indices def hasColors(self) -> bool: return self._colors is not None def getColors(self) -> numpy.ndarray: return self._colors def hasUVCoordinates(self) -> bool: return self._uvs is not None def getFileName(self) -> Optional[str]: return self._file_name def getTransformed(self, transformation: Matrix) -> "MeshData": """Transform the meshdata, center and zero position by given Matrix :param transformation: 4x4 homogeneous transformation matrix """ if self._vertices is not None: transformed_vertices = transformVertices(self._vertices, transformation) transformed_normals = transformNormals( self._normals, transformation) if self._normals is not None else None transformation_matrix = transformation.getTransposed() if self._center_position is not None: center_position = self._center_position.multiply( transformation_matrix) else: center_position = Reuse zero_position = self._zero_position.multiply(transformation_matrix) return self.set(vertices=transformed_vertices, normals=transformed_normals, center_position=center_position, zero_position=zero_position) else: return MeshData(vertices=self._vertices) def getExtents(self, matrix: Optional[Matrix] = None ) -> Optional[AxisAlignedBox]: """Get the extents of this mesh. :param matrix: The transformation matrix from model to world coordinates. """ if self._vertices is None: return None if matrix is not None: data = self.getConvexHullTransformedVertices(matrix) else: data = self.getConvexHullVertices() if data is None: return None min = data.min(axis=0) max = data.max(axis=0) return AxisAlignedBox(minimum=Vector(min[0], min[1], min[2]), maximum=Vector(max[0], max[1], max[2])) def getVerticesAsByteArray(self) -> Optional[bytes]: """Get all vertices of this mesh as a bytearray :return: A bytearray object with 3 floats per vertex. """ if self._vertices is None: return None return self._vertices.tostring() def getNormalsAsByteArray(self) -> Optional[bytes]: """Get all normals of this mesh as a bytearray :return: A bytearray object with 3 floats per normal. """ if self._normals is None: return None return self._normals.tostring() def getIndicesAsByteArray(self) -> Optional[bytes]: """Get all indices as a bytearray :return: A bytearray object with 3 ints per face. """ if self._indices is None: return None return self._indices.tostring() def getColorsAsByteArray(self) -> Optional[bytes]: if self._colors is None: return None return self._colors.tostring() def getUVCoordinatesAsByteArray(self) -> Optional[bytes]: if self._uvs is None: return None return self._uvs.tostring() def _computeConvexHull(self) -> None: """Convex hull handling""" points = self.getVertices() if points is None: return self._convex_hull = approximateConvexHull(points, MAXIMUM_HULL_VERTICES_COUNT) def getConvexHull(self) -> Optional[scipy.spatial.ConvexHull]: """Gets the Convex Hull of this mesh :return: :type{scipy.spatial.ConvexHull} """ with self._convex_hull_lock: if self._convex_hull is None: self._computeConvexHull() return self._convex_hull def getConvexHullVertices(self) -> Optional[numpy.ndarray]: """Gets the convex hull points :return: :type{numpy.ndarray} the vertices which describe the convex hull """ if self._convex_hull_vertices is None: convex_hull = self.getConvexHull() if convex_hull is None: return None self._convex_hull_vertices = numpy.take(convex_hull.points, convex_hull.vertices, axis=0) return self._convex_hull_vertices def getConvexHullTransformedVertices( self, transformation: Matrix) -> Optional[numpy.ndarray]: """Gets transformed convex hull points :return: :type{numpy.ndarray} the vertices which describe the convex hull """ vertices = self.getConvexHullVertices() if vertices is not None: return transformVertices(vertices, transformation) else: return None def getFacePlane(self, face_id: int) -> Tuple[numpy.ndarray, numpy.ndarray]: """Gets the plane the supplied face lies in. The resultant plane is specified by a point and a normal. :param face_id: :type{int} The index of the face (not the flattened indices). :return: :type{Tuple[numpy.ndarray, numpy.ndarray]} A plane, the 1st vector is the center, the 2nd the normal. """ v_a, v_b, v_c = self.getFaceNodes(face_id) in_point = (v_a + v_b + v_c) / 3.0 face_normal = numpy.cross(v_b - v_a, v_c - v_a) return in_point, face_normal def getFaceNodes( self, face_id: int ) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]: """Gets the node vectors of the supplied face. :param face_id: :type{int} The index of the face (not the flattened indices). :return: :type{Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]} Tuple of all three local vectors. """ if self._indices is None or len(self._indices) == 0: base_index = face_id * 3 v_a = self._vertices[base_index] v_b = self._vertices[base_index + 1] v_c = self._vertices[base_index + 2] else: v_a = self._vertices[self._indices[face_id][0]] v_b = self._vertices[self._indices[face_id][1]] v_c = self._vertices[self._indices[face_id][2]] return v_a, v_b, v_c def hasAttribute(self, key: str) -> bool: return key in self._attributes def getAttribute(self, key: str): """the return value is a dict with at least keys opengl_name, opengl_type, value""" return self._attributes[key] def attributeNames(self) -> List[str]: """Return attribute names in alphabetical order The sorting assures that the order is always the same. """ if not self._attributes: return [] result = list(self._attributes.keys()) result.sort() return result def invertNormals(self) -> None: if self._normals is not None: mirror = Matrix() mirror.setToIdentity() mirror.scaleByFactor(-1.0) self._normals = transformNormals(self._normals, mirror) if self._indices is not None: new_indices = [] for face in self._indices: new_indices.append([face[1], face[0], face[2]]) self._indices = NumPyUtil.immutableNDArray(new_indices) self._indices_byte_array = None else: new_vertices = [] num_vertices = len(self._vertices) for i in range(0, num_vertices, 3): new_vertices.append(self._vertices[i + 1]) new_vertices.append(self._vertices[i]) new_vertices.append(self._vertices[i + 2]) self._vertices = NumPyUtil.immutableNDArray(new_vertices) def toString(self) -> str: return "MeshData(_vertices=" + str(self._vertices) + ", _normals=" + str(self._normals) + ", _indices=" + \ str(self._indices) + ", _colors=" + str(self._colors) + ", _uvs=" + str(self._uvs) + ", _attributes=" + \ str(self._attributes.keys()) + ") "
class MeshData: def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type = MeshType.faces, attributes=None): self._application = None # Initialize this later otherwise unit tests break self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) self._indices = NumPyUtil.immutableNDArray(indices) self._colors = NumPyUtil.immutableNDArray(colors) self._uvs = NumPyUtil.immutableNDArray(uvs) self._vertex_count = len(self._vertices) if self._vertices is not None else 0 self._face_count = len(self._indices) if self._indices is not None else 0 self._type = type self._file_name = file_name # type: Optional[str] # original center position self._center_position = center_position # original zero position, is changed after transformation if zero_position is not None: self._zero_position = zero_position else: self._zero_position = Vector(0, 0, 0) # type: Vector self._convex_hull = None # type: Optional[scipy.spatial.ConvexHull] self._convex_hull_vertices = None # type: Optional[numpy.ndarray] self._convex_hull_lock = threading.Lock() self._attributes = {} if attributes is not None: for key, attribute in attributes.items(): new_value = {} for attribute_key, attribute_value in attribute.items(): if attribute_key == "value": new_value["value"] = NumPyUtil.immutableNDArray(attribute_value) else: new_value[attribute_key] = attribute_value self._attributes[key] = new_value ## Triggered when this file is deleted. # # The file will then no longer be watched for changes. def __del__(self): if self._file_name: if self._application: self._application.getController().getScene().removeWatchedFile(self._file_name) ## Create a new MeshData with specified changes # \return \type{MeshData} def set(self, vertices=Reuse, normals=Reuse, indices=Reuse, colors=Reuse, uvs=Reuse, file_name=Reuse, center_position=Reuse, zero_position=Reuse, attributes=Reuse) -> "MeshData": vertices = vertices if vertices is not Reuse else self._vertices normals = normals if normals is not Reuse else self._normals indices = indices if indices is not Reuse else self._indices colors = colors if colors is not Reuse else self._colors uvs = uvs if uvs is not Reuse else self._uvs file_name = file_name if file_name is not Reuse else self._file_name center_position = center_position if center_position is not Reuse else self._center_position zero_position = zero_position if zero_position is not Reuse else self._zero_position attributes = attributes if attributes is not Reuse else self._attributes return MeshData(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, file_name=file_name, center_position=center_position, zero_position=zero_position, attributes=attributes) def getHash(self): m = hashlib.sha256() m.update(self.getVerticesAsByteArray()) return m.hexdigest() def getCenterPosition(self) -> Vector: return self._center_position def getZeroPosition(self) -> Vector: return self._zero_position def getType(self): return self._type def getFaceCount(self) -> int: return self._face_count ## Get the array of vertices def getVertices(self) -> numpy.ndarray: return self._vertices ## Get the number of vertices def getVertexCount(self) -> int: return self._vertex_count ## Get a vertex by index def getVertex(self, index): try: return self._vertices[index] except IndexError: return None ## Return whether this mesh has vertex normals. def hasNormals(self) -> bool: return self._normals is not None ## Return the list of vertex normals. def getNormals(self) -> numpy.ndarray: return self._normals ## Return whether this mesh has indices. def hasIndices(self) -> bool: return self._indices is not None ## Get the array of indices # \return \type{numpy.ndarray} def getIndices(self) -> numpy.ndarray: return self._indices def hasColors(self) -> bool: return self._colors is not None def getColors(self) -> numpy.ndarray: return self._colors def hasUVCoordinates(self) -> bool: return self._uvs is not None def getFileName(self) -> Optional[str]: return self._file_name ## Transform the meshdata, center and zero position by given Matrix # \param transformation 4x4 homogenous transformation matrix def getTransformed(self, transformation: Matrix) -> "MeshData": if self._vertices is not None: transformed_vertices = transformVertices(self._vertices, transformation) transformed_normals = transformNormals(self._normals, transformation) if self._normals is not None else None transformation_matrix = transformation.getTransposed() if self._center_position is not None: center_position = self._center_position.multiply(transformation_matrix) else: center_position = Reuse zero_position = self._zero_position.multiply(transformation_matrix) return self.set(vertices=transformed_vertices, normals=transformed_normals, center_position=center_position, zero_position=zero_position) else: return MeshData(vertices = self._vertices) ## Get the extents of this mesh. # # \param matrix The transformation matrix from model to world coordinates. def getExtents(self, matrix: Optional[Matrix] = None) -> Optional[AxisAlignedBox]: if self._vertices is None: return None if matrix is not None: data = self.getConvexHullTransformedVertices(matrix) else: data = self.getConvexHullVertices() if data is None: return None min = data.min(axis=0) max = data.max(axis=0) return AxisAlignedBox(minimum=Vector(min[0], min[1], min[2]), maximum=Vector(max[0], max[1], max[2])) ## Get all vertices of this mesh as a bytearray # # \return A bytearray object with 3 floats per vertex. def getVerticesAsByteArray(self) -> Optional[bytes]: if self._vertices is None: return None return self._vertices.tostring() ## Get all normals of this mesh as a bytearray # # \return A bytearray object with 3 floats per normal. def getNormalsAsByteArray(self) -> Optional[bytes]: if self._normals is None: return None return self._normals.tostring() ## Get all indices as a bytearray # # \return A bytearray object with 3 ints per face. def getIndicesAsByteArray(self) -> Optional[bytes]: if self._indices is None: return None return self._indices.tostring() def getColorsAsByteArray(self) -> Optional[bytes]: if self._colors is None: return None return self._colors.tostring() def getUVCoordinatesAsByteArray(self) -> Optional[bytes]: if self._uvs is None: return None return self._uvs.tostring() ####################################################################### # Convex hull handling ####################################################################### def _computeConvexHull(self): points = self.getVertices() if points is None: return self._convex_hull = approximateConvexHull(points, MAXIMUM_HULL_VERTICES_COUNT) ## Gets the Convex Hull of this mesh # # \return \type{scipy.spatial.ConvexHull} def getConvexHull(self) -> Optional[scipy.spatial.ConvexHull]: with self._convex_hull_lock: if self._convex_hull is None: self._computeConvexHull() return self._convex_hull ## Gets the convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullVertices(self) -> Optional[numpy.ndarray]: if self._convex_hull_vertices is None: convex_hull = self.getConvexHull() if convex_hull is None: return None self._convex_hull_vertices = numpy.take(convex_hull.points, convex_hull.vertices, axis=0) return self._convex_hull_vertices ## Gets transformed convex hull points # # \return \type{numpy.ndarray} the vertices which describe the convex hull def getConvexHullTransformedVertices(self, transformation: Matrix) -> Optional[numpy.ndarray]: vertices = self.getConvexHullVertices() if vertices is not None: return transformVertices(vertices, transformation) else: return None def hasAttribute(self, key: str) -> bool: return key in self._attributes ## the return value is a dict with at least keys opengl_name, opengl_type, value def getAttribute(self, key: str): return self._attributes[key] ## Return attribute names in alphabetical order # The sorting assures that the order is always the same. def attributeNames(self) -> List[str]: result = list(self._attributes.keys()) result.sort() return result def toString(self) -> str: return "MeshData(_vertices=" + str(self._vertices) + ", _normals=" + str(self._normals) + ", _indices=" + \ str(self._indices) + ", _colors=" + str(self._colors) + ", _uvs=" + str(self._uvs) + ", _attributes=" + \ str(self._attributes.keys()) + ") "