"""github.com/mikedh/trimesh----------------------------Library for importing, exporting and doing simple operations on triangular meshes."""importcopyimportwarningsimportnumpyasnpfromnumpyimportfloat64,int64,ndarrayfrom.import(boolean,comparison,convex,curvature,decomposition,geometry,graph,grouping,inertia,intersections,permutate,poses,proximity,ray,registration,remesh,repair,sample,transformations,triangles,units,util,)from.cachingimportCache,DataStore,TrackedArray,cache_decoratorfrom.constantsimportlog,tolfrom.exceptionsimportExceptionWrapperfrom.exchange.exportimportexport_meshfrom.parentimportGeometry3Dfrom.sceneimportScenefrom.trianglesimportMassPropertiesfrom.typedimport(Any,ArrayLike,Dict,Floating,Integer,List,NDArray,Number,Optional,Sequence,Union,)from.visualimportColorVisuals,TextureVisuals,create_visualtry:fromscipy.sparseimportcoo_matrixfromscipy.spatialimportcKDTreeexceptBaseExceptionasE:cKDTree=ExceptionWrapper(E)coo_matrix=ExceptionWrapper(E)try:fromnetworkximportGraphexceptBaseExceptionasE:Graph=ExceptionWrapper(E)try:fromrtree.indeximportIndexexceptBaseExceptionasE:Index=ExceptionWrapper(E)try:from.pathimportPath2D,Path3DexceptBaseExceptionasE:Path2D=ExceptionWrapper(E)Path3D=ExceptionWrapper(E)# save immutable identity matrices for checks_IDENTITY3=np.eye(3,dtype=np.float64)_IDENTITY3.flags.writeable=False_IDENTITY4=np.eye(4,dtype=np.float64)_IDENTITY4.flags.writeable=False
[docs]def__init__(self,vertices:Optional[ArrayLike]=None,faces:Optional[ArrayLike]=None,face_normals:Optional[ArrayLike]=None,vertex_normals:Optional[ArrayLike]=None,face_colors:Optional[ArrayLike]=None,vertex_colors:Optional[ArrayLike]=None,face_attributes:Optional[Dict[str,ArrayLike]]=None,vertex_attributes:Optional[Dict[str,ArrayLike]]=None,metadata:Optional[Dict[str,Any]]=None,process:bool=True,validate:bool=False,merge_tex:Optional[bool]=None,merge_norm:Optional[bool]=None,use_embree:bool=True,initial_cache:Optional[Dict[str,ndarray]]=None,visual:Optional[Union[ColorVisuals,TextureVisuals]]=None,**kwargs,)->None:""" A Trimesh object contains a triangular 3D mesh. Parameters ------------ vertices : (n, 3) float Array of vertex locations faces : (m, 3) or (m, 4) int Array of triangular or quad faces (triangulated on load) face_normals : (m, 3) float Array of normal vectors corresponding to faces vertex_normals : (n, 3) float Array of normal vectors for vertices metadata : dict Any metadata about the mesh process : bool if True, Nan and Inf values will be removed immediately and vertices will be merged validate : bool If True, degenerate and duplicate faces will be removed immediately, and some functions will alter the mesh to ensure consistent results. use_embree : bool If True try to use pyembree raytracer. If pyembree is not available it will automatically fall back to a much slower rtree/numpy implementation initial_cache : dict A way to pass things to the cache in case expensive things were calculated before creating the mesh object. visual : ColorVisuals or TextureVisuals Assigned to self.visual """# self._data stores information about the mesh which# CANNOT be regenerated.# in the base class all that is stored here is vertex and# face information# any data put into the store is converted to a TrackedArray# which is a subclass of np.ndarray that provides hash and crc# methods which can be used to detect changes in the array.self._data=DataStore()# self._cache stores information about the mesh which CAN be# regenerated from self._data, but may be slow to calculate.# In order to maintain consistency# the cache is cleared when self._data.__hash__() changesself._cache=Cache(id_function=self._data.__hash__,force_immutable=True)ifinitial_cacheisnotNone:self._cache.update(initial_cache)# check for None only to avoid warning messages in subclasses# (n, 3) float array of verticesself.vertices=vertices# (m, 3) int of triangle faces that references self.verticesself.faces=faces# hold visual information about the mesh (vertex and face colors)ifvisualisNone:self.visual=create_visual(face_colors=face_colors,vertex_colors=vertex_colors,mesh=self)else:self.visual=visual# normals are accessed through setters/properties and are regenerated# if dimensions are inconsistent, but can be set by the constructor# to avoid a substantial number of cross productsifface_normalsisnotNone:self.face_normals=face_normals# (n, 3) float of vertex normals, can be created from face normalsifvertex_normalsisnotNone:self.vertex_normals=vertex_normals# embree is a much, much faster raytracer written by Intel# if you have pyembree installed you should use it# although both raytracers were designed to have a common APIifray.has_embreeanduse_embree:self.ray=ray.ray_pyembree.RayMeshIntersector(self)else:# create a ray-mesh query object for the current mesh# initializing is very inexpensive and object is convenient to have.# On first query expensive bookkeeping is done (creation of r-tree),# and is cached for subsequent queriesself.ray=ray.ray_triangle.RayMeshIntersector(self)# a quick way to get permuted versions of the current meshself.permutate=permutate.Permutator(self)# convenience class for nearest point queriesself.nearest=proximity.ProximityQuery(self)# update the mesh metadata with passed metadataself.metadata={}ifisinstance(metadata,dict):self.metadata.update(metadata)elifmetadataisnotNone:raiseValueError(f"metadata should be a dict or None, got {metadata!s}")# store per-face and per-vertex attributes which will# be updated when an update_faces call is madeself.face_attributes={}self.vertex_attributes={}# use update to copy itemsifface_attributesisnotNone:self.face_attributes.update(face_attributes)ifvertex_attributesisnotNone:self.vertex_attributes.update(vertex_attributes)# process will remove NaN and Inf values and merge vertices# if validate, will remove degenerate and duplicate facesifprocessorvalidate:self.process(validate=validate,merge_tex=merge_tex,merge_norm=merge_norm)
[docs]defprocess(self,validate:bool=False,merge_tex:Optional[bool]=None,merge_norm:Optional[bool]=None,)->"Trimesh":""" Do processing to make a mesh useful. Does this by: 1) removing NaN and Inf values 2) merging duplicate vertices If validate: 3) Remove triangles which have one edge of their 2D oriented bounding box shorter than tol.merge 4) remove duplicated triangles 5) Attempt to ensure triangles are consistently wound and normals face outwards. Parameters ------------ validate : bool Remove degenerate and duplicate faces. Returns ------------ self: trimesh.Trimesh Current mesh """# if there are no vertices or faces exit earlyifself.is_empty:returnself# avoid clearing the cache during operationswithself._cache:# if we're cleaning remove duplicate# and degenerate facesifvalidate:# get a mask with only unique and non-degenerate facesmask=self.unique_faces()&self.nondegenerate_faces()self.update_faces(mask)self.fix_normals()# since none of our process operations moved vertices or faces# we can keep face and vertex normals in the cache without recomputing# if faces or vertices have been removed, normals are validated before# being returned so there is no danger of inconsistent dimensionsself.remove_infinite_values()self.merge_vertices(merge_tex=merge_tex,merge_norm=merge_norm)self._cache.clear(exclude={"face_normals","vertex_normals"})self.metadata["processed"]=Truereturnself
@propertydefmutable(self)->bool:""" Is the current mesh allowed to be altered in-place? Returns ------------- mutable If data is allowed to be set for the mesh. """returnself._data.mutable@mutable.setterdefmutable(self,value:bool)->None:""" Set the mutability of the current mesh. Parameters ---------- value Change whether the current mesh is allowed to be altered in-place. """self._data.mutable=value@propertydeffaces(self)->TrackedArray:""" The faces of the mesh. This is regarded as core information which cannot be regenerated from cache and as such is stored in `self._data` which tracks the array for changes and clears cached values of the mesh altered. Returns ---------- faces : (n, 3) int64 References for `self.vertices` for triangles. """returnself._data["faces"]@faces.setterdeffaces(self,values:Optional[ArrayLike])->None:""" Set the vertex indexes that make up triangular faces. Parameters -------------- values : (n, 3) int64 Indexes of self.vertices """ifvaluesisNone:# if passed none store an empty arrayvalues=np.zeros(shape=(0,3),dtype=int64)else:values=np.asanyarray(values,dtype=int64)# automatically triangulate quad facesiflen(values.shape)==2andvalues.shape[1]!=3:log.info("triangulating faces")values=geometry.triangulate_quads(values)self._data["faces"]=values@cache_decoratordeffaces_sparse(self)->coo_matrix:""" A sparse matrix representation of the faces. Returns ---------- sparse : scipy.sparse.coo_matrix Has properties: dtype : bool shape : (len(self.vertices), len(self.faces)) """returngeometry.index_sparse(columns=len(self.vertices),indices=self.faces)@propertydefface_normals(self)->NDArray[float64]:""" Return the unit normal vector for each face. If a face is degenerate and a normal can't be generated a zero magnitude unit vector will be returned for that face. Returns ----------- normals : (len(self.faces), 3) float64 Normal vectors of each face """# check shape of cached normalscached=self._cache["face_normals"]# get faces from datastoreif"faces"inself._data:faces=self._data.data["faces"]else:faces=None# if we have no faces exit earlyiffacesisNoneorlen(faces)==0:returnnp.array([],dtype=float64).reshape((0,3))# if the shape of cached normals equals the shape of faces returnifnp.shape(cached)==np.shape(faces):returncached# use cached triangle cross products to generate normals# this will always return the correct shape but some values# will be zero or an arbitrary vector if the inputs had# a cross product below machine epsilonnormals,valid=triangles.normals(triangles=self.triangles,crosses=self.triangles_cross)# if all triangles are valid shape is correctifvalid.all():# put calculated face normals into cache manuallyself._cache["face_normals"]=normalsreturnnormals# make a padded list of normals for correct shapepadded=np.zeros((len(self.triangles),3),dtype=float64)padded[valid]=normals# put calculated face normals into cache manuallyself._cache["face_normals"]=paddedreturnpadded@face_normals.setterdefface_normals(self,values:Optional[ArrayLike])->None:""" Assign values to face normals. Parameters ------------- values : (len(self.faces), 3) float Unit face normals. If None will clear existing normals. """# if nothing passed exitifvaluesisNone:return# make sure candidate face normals are C-contiguous floatvalues=np.asanyarray(values,order="C",dtype=float64)# face normals need to correspond to facesiflen(values)==0orvalues.shape!=self.faces.shape:log.debug("face_normals incorrect shape, ignoring!")return# check if any values are larger than tol.merge# don't set the normals if they are all zeroptp=np.ptp(values)ifnotnp.isfinite(ptp):log.debug("face_normals contain NaN, ignoring!")returnifptp<tol.merge:log.debug("face_normals all zero, ignoring!")return# make sure the first few normals match the first few trianglescheck,valid=triangles.normals(self.vertices.view(np.ndarray)[self.faces[:20]])compare=np.zeros((len(valid),3))compare[valid]=checkifnotnp.allclose(compare,values[:20]):log.debug("face_normals didn't match triangles, ignoring!")return# otherwise store face normalsself._cache["face_normals"]=values@propertydefvertices(self)->TrackedArray:""" The vertices of the mesh. This is regarded as core information which cannot be generated from cache and as such is stored in self._data which tracks the array for changes and clears cached values of the mesh if this is altered. Returns ---------- vertices : (n, 3) float Points in cartesian space referenced by self.faces """# get vertices if already storedreturnself._data["vertices"]@vertices.setterdefvertices(self,values:Optional[ArrayLike]):""" Assign vertex values to the mesh. Parameters -------------- values : (n, 3) float Points in space """ifvaluesisNone:# remove any stored data and store an empty arrayvalues=np.zeros(shape=(0,3),dtype=float64)self._data["vertices"]=np.asanyarray(values,order="C",dtype=float64)@cache_decoratordefvertex_normals(self)->NDArray[float64]:""" The vertex normals of the mesh. If the normals were loaded we check to make sure we have the same number of vertex normals and vertices before returning them. If there are no vertex normals defined or a shape mismatch we calculate the vertex normals from the mean normals of the faces the vertex is used in. Returns ---------- vertex_normals : (n, 3) float Represents the surface normal at each vertex. Where n == len(self.vertices) """# make sure we have faces_sparseasserthasattr(self.faces_sparse,"dot")returngeometry.weighted_vertex_normals(vertex_count=len(self.vertices),faces=self.faces,face_normals=self.face_normals,face_angles=self.face_angles,)@vertex_normals.setterdefvertex_normals(self,values:ArrayLike)->None:""" Assign values to vertex normals. Parameters ------------- values : (len(self.vertices), 3) float Unit normal vectors for each vertex """ifvaluesisnotNone:values=np.asanyarray(values,order="C",dtype=float64)ifvalues.shape==self.vertices.shape:# check to see if they assigned all zerosifnp.ptp(values)<tol.merge:log.debug("vertex_normals are all zero!")self._cache["vertex_normals"]=values@cache_decoratordefvertex_faces(self)->NDArray[int64]:""" A representation of the face indices that correspond to each vertex. Returns ---------- vertex_faces : (n,m) int Each row contains the face indices that correspond to the given vertex, padded with -1 up to the max number of faces corresponding to any one vertex Where n == len(self.vertices), m == max number of faces for a single vertex """vertex_faces=geometry.vertex_face_indices(vertex_count=len(self.vertices),faces=self.faces,faces_sparse=self.faces_sparse,)returnvertex_faces@cache_decoratordefbounds(self)->Optional[NDArray[float64]]:""" The axis aligned bounds of the faces of the mesh. Returns ----------- bounds : (2, 3) float or None Bounding box with [min, max] coordinates If mesh is empty will return None """# return bounds including ONLY referenced verticesin_mesh=self.vertices[self.referenced_vertices]# don't crash if we have no vertices referencediflen(in_mesh)==0:returnNone# get mesh bounds with min and maxreturnnp.array([in_mesh.min(axis=0),in_mesh.max(axis=0)])@cache_decoratordefextents(self)->Optional[NDArray[float64]]:""" The length, width, and height of the axis aligned bounding box of the mesh. Returns ----------- extents : (3, ) float or None Array containing axis aligned [length, width, height] If mesh is empty returns None """# if mesh is empty return Noneifself.boundsisNone:returnNoneextents=np.ptp(self.bounds,axis=0)returnextents@cache_decoratordefcentroid(self)->NDArray[float64]:""" The point in space which is the average of the triangle centroids weighted by the area of each triangle. This will be valid even for non-watertight meshes, unlike self.center_mass Returns ---------- centroid : (3, ) float The average vertex weighted by face area """# use the centroid of each triangle weighted by# the area of the triangle to find the overall centroidtry:centroid=np.average(self.triangles_center,weights=self.area_faces,axis=0)exceptBaseException:# if all triangles are zero-area weights will not workcentroid=self.triangles_center.mean(axis=0)returncentroid@propertydefcenter_mass(self)->NDArray[float64]:""" The point in space which is the center of mass/volume. Returns ----------- center_mass : (3, ) float Volumetric center of mass of the mesh. """returnself.mass_properties.center_mass@center_mass.setterdefcenter_mass(self,value:ArrayLike)->None:""" Override the point in space which is the center of mass and volume. Parameters ----------- center_mass : (3, ) float Volumetric center of mass of the mesh. """value=np.array(value,dtype=float64)ifvalue.shape!=(3,):raiseValueError("shape must be (3,) float!")self._data["center_mass"]=valueself._cache.delete("mass_properties")@propertydefdensity(self)->float:""" The density of the mesh used in inertia calculations. Returns ----------- density The density of the primitive. """returnfloat(self.mass_properties.density)@density.setterdefdensity(self,value:Number)->None:""" Set the density of the primitive. Parameters ------------- density Specify the density of the primitive to be used in inertia calculations. """self._data["density"]=float(value)self._cache.delete("mass_properties")@propertydefvolume(self)->float64:""" Volume of the current mesh calculated using a surface integral. If the current mesh isn't watertight this is garbage. Returns --------- volume : float Volume of the current mesh """returnself.mass_properties.volume@propertydefmass(self)->float64:""" Mass of the current mesh, based on specified density and volume. If the current mesh isn't watertight this is garbage. Returns --------- mass : float Mass of the current mesh """returnself.mass_properties.mass@propertydefmoment_inertia(self)->NDArray[float64]:""" Return the moment of inertia matrix of the current mesh. If mesh isn't watertight this is garbage. The returned moment of inertia is *axis aligned* at the mesh's center of mass `mesh.center_mass`. If you want the moment at any other frame including the origin call: `mesh.moment_inertia_frame` Returns --------- inertia : (3, 3) float Moment of inertia of the current mesh at the center of mass and aligned with the cartesian axis. """returnself.mass_properties.inertia
[docs]defmoment_inertia_frame(self,transform:ArrayLike)->NDArray[float64]:""" Get the moment of inertia of this mesh with respect to an arbitrary frame, versus with respect to the center of mass as returned by `mesh.moment_inertia`. For example if `transform` is an identity matrix `np.eye(4)` this will give the moment at the origin. Uses the parallel axis theorum to move the center mass tensor to this arbitrary frame. Parameters ------------ transform : (4, 4) float Homogeneous transformation matrix. Returns ------------- inertia : (3, 3) Moment of inertia in the requested frame. """# we'll need the inertia tensor and the center of massprops=self.mass_properties# calculated moment of inertia is at the center of mass# so we want to offset our requested translation by that# center of massoffset=np.eye(4)offset[:3,3]=-props["center_mass"]# apply the parallel axis theorum to get the new inertiareturninertia.transform_inertia(inertia_tensor=props["inertia"],transform=np.dot(offset,transform),mass=props["mass"],parallel_axis=True,)
@cache_decoratordefprincipal_inertia_components(self)->NDArray[float64]:""" Return the principal components of inertia Ordering corresponds to mesh.principal_inertia_vectors Returns ---------- components : (3, ) float Principal components of inertia """# both components and vectors from inertia matrixcomponents,vectors=inertia.principal_axis(self.moment_inertia)# store vectors in cache for laterself._cache["principal_inertia_vectors"]=vectorsreturncomponents@propertydefprincipal_inertia_vectors(self)->NDArray[float64]:""" Return the principal axis of inertia as unit vectors. The order corresponds to `mesh.principal_inertia_components`. Returns ---------- vectors : (3, 3) float Three vectors pointing along the principal axis of inertia directions """_=self.principal_inertia_componentsreturnself._cache["principal_inertia_vectors"]@cache_decoratordefprincipal_inertia_transform(self)->NDArray[float64]:""" A transform which moves the current mesh so the principal inertia vectors are on the X,Y, and Z axis, and the centroid is at the origin. Returns ---------- transform : (4, 4) float Homogeneous transformation matrix """order=np.argsort(self.principal_inertia_components)[1:][::-1]vectors=self.principal_inertia_vectors[order]vectors=np.vstack((vectors,np.cross(*vectors)))transform=np.eye(4)transform[:3,:3]=vectorstransform=transformations.transform_around(matrix=transform,point=self.centroid)transform[:3,3]-=self.centroidreturntransform@cache_decoratordefsymmetry(self)->Optional[str]:""" Check whether a mesh has rotational symmetry around an axis (radial) or point (spherical). Returns ----------- symmetry : None, 'radial', 'spherical' What kind of symmetry does the mesh have. """symmetry,axis,section=inertia.radial_symmetry(self)self._cache["symmetry_axis"]=axisself._cache["symmetry_section"]=sectionreturnsymmetry@propertydefsymmetry_axis(self)->Optional[NDArray[float64]]:""" If a mesh has rotational symmetry, return the axis. Returns ------------ axis : (3, ) float Axis around which a 2D profile was revolved to create this mesh. """ifself.symmetryisNone:returnNonereturnself._cache["symmetry_axis"]@propertydefsymmetry_section(self)->Optional[NDArray[float64]]:""" If a mesh has rotational symmetry return the two vectors which make up a section coordinate frame. Returns ---------- section : (2, 3) float Vectors to take a section along """ifself.symmetryisNone:returnNonereturnself._cache["symmetry_section"]@cache_decoratordeftriangles(self)->NDArray[float64]:""" Actual triangles of the mesh (points, not indexes) Returns --------- triangles : (n, 3, 3) float Points of triangle vertices """# use of advanced indexing on our tracked arrays will# trigger a change flag which means the hash will have to be# recomputed. We can escape this check by viewing the array.returnself.vertices.view(np.ndarray)[self.faces]@cache_decoratordeftriangles_tree(self)->Index:""" An R-tree containing each face of the mesh. Returns ---------- tree : rtree.index Each triangle in self.faces has a rectangular cell """returntriangles.bounds_tree(self.triangles)@cache_decoratordeftriangles_center(self)->NDArray[float64]:""" The center of each triangle (barycentric [1/3, 1/3, 1/3]) Returns --------- triangles_center : (len(self.faces), 3) float Center of each triangular face """returnself.triangles.mean(axis=1)@cache_decoratordeftriangles_cross(self)->NDArray[float64]:""" The cross product of two edges of each triangle. Returns --------- crosses : (n, 3) float Cross product of each triangle """crosses=triangles.cross(self.triangles)returncrosses@cache_decoratordefedges(self)->NDArray[int64]:""" Edges of the mesh (derived from faces). Returns --------- edges : (n, 2) int List of vertex indices making up edges """edges,index=geometry.faces_to_edges(self.faces.view(np.ndarray),return_index=True)self._cache["edges_face"]=indexreturnedges@cache_decoratordefedges_face(self)->NDArray[int64]:""" Which face does each edge belong to. Returns --------- edges_face : (n, ) int Index of self.faces """_=self.edgesreturnself._cache["edges_face"]@cache_decoratordefedges_unique(self)->NDArray[int64]:""" The unique edges of the mesh. Returns ---------- edges_unique : (n, 2) int Vertex indices for unique edges """unique,inverse=grouping.unique_rows(self.edges_sorted)edges_unique=self.edges_sorted[unique]# edges_unique will be added automatically by the decorator# additional terms generated need to be added to the cache manuallyself._cache["edges_unique_idx"]=uniqueself._cache["edges_unique_inverse"]=inversereturnedges_unique@cache_decoratordefedges_unique_length(self)->NDArray[float64]:""" How long is each unique edge. Returns ---------- length : (len(self.edges_unique), ) float Length of each unique edge """vector=np.subtract(*self.vertices[self.edges_unique.T])length=util.row_norm(vector)returnlength@cache_decoratordefedges_unique_inverse(self)->NDArray[int64]:""" Return the inverse required to reproduce self.edges_sorted from self.edges_unique. Useful for referencing edge properties: mesh.edges_unique[mesh.edges_unique_inverse] == m.edges_sorted Returns ---------- inverse : (len(self.edges), ) int Indexes of self.edges_unique """_=self.edges_uniquereturnself._cache["edges_unique_inverse"]@cache_decoratordefedges_sorted(self)->NDArray[int64]:""" Edges sorted along axis 1 Returns ---------- edges_sorted : (n, 2) Same as self.edges but sorted along axis 1 """edges_sorted=np.sort(self.edges,axis=1)returnedges_sorted@cache_decoratordefedges_sorted_tree(self)->cKDTree:""" A KDTree for mapping edges back to edge index. Returns ------------ tree : scipy.spatial.cKDTree Tree when queried with edges will return their index in mesh.edges_sorted """returncKDTree(self.edges_sorted)@cache_decoratordefedges_sparse(self)->coo_matrix:""" Edges in sparse bool COO graph format where connected vertices are True. Returns ---------- sparse: (len(self.vertices), len(self.vertices)) bool Sparse graph in COO format """sparse=graph.edges_to_coo(self.edges,count=len(self.vertices))returnsparse@cache_decoratordefbody_count(self)->int:""" How many connected groups of vertices exist in this mesh. Note that this number may differ from result in mesh.split, which is calculated from FACE rather than vertex adjacency. Returns ----------- count : int Number of connected vertex groups """# labels are (len(vertices), int) OBcount,labels=graph.csgraph.connected_components(self.edges_sparse,directed=False,return_labels=True)self._cache["vertices_component_label"]=labelsreturncount@cache_decoratordeffaces_unique_edges(self)->NDArray[int64]:""" For each face return which indexes in mesh.unique_edges constructs that face. Returns --------- faces_unique_edges : (len(self.faces), 3) int Indexes of self.edges_unique that construct self.faces Examples --------- In [0]: mesh.faces[:2] Out[0]: TrackedArray([[ 1, 6946, 24224], [ 6946, 1727, 24225]]) In [1]: mesh.edges_unique[mesh.faces_unique_edges[:2]] Out[1]: array([[[ 1, 6946], [ 6946, 24224], [ 1, 24224]], [[ 1727, 6946], [ 1727, 24225], [ 6946, 24225]]]) """# make sure we have populated unique edges_=self.edges_unique# we are relying on the fact that edges are stacked in tripletsresult=self._cache["edges_unique_inverse"].reshape((-1,3))returnresult@cache_decoratordefeuler_number(self)->int:""" Return the Euler characteristic (a topological invariant) for the mesh In order to guarantee correctness, this should be called after remove_unreferenced_vertices Returns ---------- euler_number : int Topological invariant """returnint(self.referenced_vertices.sum()-len(self.edges_unique)+len(self.faces))@cache_decoratordefreferenced_vertices(self)->NDArray[np.bool_]:""" Which vertices in the current mesh are referenced by a face. Returns ------------- referenced : (len(self.vertices), ) bool Which vertices are referenced by a face """referenced=np.zeros(len(self.vertices),dtype=bool)referenced[self.faces]=Truereturnreferenced
[docs]defconvert_units(self,desired:str,guess:bool=False)->"Trimesh":""" Convert the units of the mesh into a specified unit. Parameters ------------ desired : string Units to convert to (eg 'inches') guess : boolean If self.units are not defined should we guess the current units of the document and then convert? """units._convert_units(self,desired,guess)returnself
[docs]defmerge_vertices(self,merge_tex:Optional[bool]=None,merge_norm:Optional[bool]=None,digits_vertex:Optional[Integer]=None,digits_norm:Optional[Integer]=None,digits_uv:Optional[Integer]=None,)->None:""" Removes duplicate vertices grouped by position and optionally texture coordinate and normal. Parameters ------------- mesh : Trimesh object Mesh to merge vertices on merge_tex : bool If True textured meshes with UV coordinates will have vertices merged regardless of UV coordinates merge_norm : bool If True, meshes with vertex normals will have vertices merged ignoring different normals digits_vertex : None or int Number of digits to consider for vertex position digits_norm : int Number of digits to consider for unit normals digits_uv : int Number of digits to consider for UV coordinates """grouping.merge_vertices(mesh=self,merge_tex=merge_tex,merge_norm=merge_norm,digits_vertex=digits_vertex,digits_norm=digits_norm,digits_uv=digits_uv,)
[docs]defupdate_vertices(self,mask:ArrayLike,inverse:Optional[ArrayLike]=None,)->None:""" Update vertices with a mask. Parameters ------------ vertex_mask : (len(self.vertices)) bool Array of which vertices to keep inverse : (len(self.vertices)) int Array to reconstruct vertex references such as output by np.unique """# if the mesh is already empty we can't remove anythingifself.is_empty:return# make sure mask is a numpy arraymask=np.asanyarray(mask)if(mask.dtype.name=="bool"andmask.all())orlen(mask)==0orself.is_empty:# mask doesn't remove any vertices so exit earlyreturn# create the inverse mask if not passedifinverseisNone:inverse=np.zeros(len(self.vertices),dtype=int64)ifmask.dtype.kind=="b":inverse[mask]=np.arange(mask.sum())elifmask.dtype.kind=="i":inverse[mask]=np.arange(len(mask))else:inverse=None# re-index faces from inverseifinverseisnotNoneandutil.is_shape(self.faces,(-1,3)):self.faces=inverse[self.faces.reshape(-1)].reshape((-1,3))# update the visual object with our maskself.visual.update_vertices(mask)# get the normals from cache before dumpingcached_normals=self._cache["vertex_normals"]# apply to face_attributescount=len(self.vertices)forkey,valueinself.vertex_attributes.items():try:# covers un-len'd objects as welliflen(value)!=count:raiseTypeError()exceptTypeError:continue# apply the mask to the attributeself.vertex_attributes[key]=value[mask]# actually apply the maskself.vertices=self.vertices[mask]# if we had passed vertex normals try to save themifutil.is_shape(cached_normals,(-1,3)):try:self.vertex_normals=cached_normals[mask]exceptBaseException:pass
[docs]defupdate_faces(self,mask:ArrayLike)->None:""" In many cases, we will want to remove specific faces. However, there is additional bookkeeping to do this cleanly. This function updates the set of faces with a validity mask, as well as keeping track of normals and colors. Parameters ------------ valid : (m) int or (len(self.faces)) bool Mask to remove faces """# if the mesh is already empty we can't remove anythingifself.is_empty:returnmask=np.asanyarray(mask)ifmask.dtype.name=="bool"andmask.all():# mask removes no faces so exit earlyreturn# try to save face normals before dumping cachecached_normals=self._cache["face_normals"]faces=self._data["faces"]# if Trimesh has been subclassed and faces have been moved# from data to cache, get faces from cache.ifnotutil.is_shape(faces,(-1,3)):faces=self._cache["faces"]# apply to face_attributescount=len(self.faces)forkey,valueinself.face_attributes.items():try:# covers un-len'd objects as welliflen(value)!=count:raiseTypeError()exceptTypeError:continue# apply the mask to the attributeself.face_attributes[key]=value[mask]# actually apply the maskself.faces=faces[mask]# apply to face colorsself.visual.update_faces(mask)# if our normals were the correct shape apply themifutil.is_shape(cached_normals,(-1,3)):self.face_normals=cached_normals[mask]
[docs]defremove_infinite_values(self)->None:""" Ensure that every vertex and face consists of finite numbers. This will remove vertices or faces containing np.nan and np.inf Alters `self.faces` and `self.vertices` """ifutil.is_shape(self.faces,(-1,3)):# (len(self.faces), ) bool, mask for facesface_mask=np.isfinite(self.faces).all(axis=1)self.update_faces(face_mask)ifutil.is_shape(self.vertices,(-1,3)):# (len(self.vertices), ) bool, mask for verticesvertex_mask=np.isfinite(self.vertices).all(axis=1)self.update_vertices(vertex_mask)
[docs]defunique_faces(self)->NDArray[np.bool_]:""" On the current mesh find which faces are unique. Returns -------- unique : (len(faces),) bool A mask where the first occurrence of a unique face is true. """mask=np.zeros(len(self.faces),dtype=bool)mask[grouping.unique_rows(np.sort(self.faces,axis=1))[0]]=Truereturnmask
[docs]defremove_duplicate_faces(self)->None:""" DERECATED MARCH 2024 REPLACE WITH: `mesh.update_faces(mesh.unique_faces())` """warnings.warn("`remove_duplicate_faces` is deprecated "+"and will be removed in March 2024: "+"replace with `mesh.update_faces(mesh.unique_faces())`",category=DeprecationWarning,stacklevel=2,)self.update_faces(self.unique_faces())
[docs]defrezero(self)->None:""" Translate the mesh so that all vertex vertices are positive. Alters `self.vertices`. """self.apply_translation(self.bounds[0]*-1.0)
[docs]defsplit(self,**kwargs)->List["Trimesh"]:""" Returns a list of Trimesh objects, based on face connectivity. Splits into individual components, sometimes referred to as 'bodies' Parameters ------------ only_watertight : bool Only return watertight meshes and discard remainder adjacency : None or (n, 2) int Override face adjacency with custom values Returns --------- meshes : (n, ) trimesh.Trimesh Separate bodies from original mesh """returngraph.split(self,**kwargs)
@cache_decoratordefface_adjacency(self)->NDArray[int64]:""" Find faces that share an edge i.e. 'adjacent' faces. Returns ---------- adjacency : (n, 2) int Pairs of faces which share an edge Examples --------- In [1]: mesh = trimesh.load('models/featuretype.STL') In [2]: mesh.face_adjacency Out[2]: array([[ 0, 1], [ 2, 3], [ 0, 3], ..., [1112, 949], [3467, 3475], [1113, 3475]]) In [3]: mesh.faces[mesh.face_adjacency[0]] Out[3]: TrackedArray([[ 1, 0, 408], [1239, 0, 1]], dtype=int64) In [4]: import networkx as nx In [5]: graph = nx.from_edgelist(mesh.face_adjacency) In [6]: groups = nx.connected_components(graph) """adjacency,edges=graph.face_adjacency(mesh=self,return_edges=True)self._cache["face_adjacency_edges"]=edgesreturnadjacency@cache_decoratordefface_neighborhood(self)->NDArray[int64]:""" Find faces that share a vertex i.e. 'neighbors' faces. Returns ---------- neighborhood : (n, 2) int Pairs of faces which share a vertex """returngraph.face_neighborhood(self)@cache_decoratordefface_adjacency_edges(self)->NDArray[int64]:""" Returns the edges that are shared by the adjacent faces. Returns -------- edges : (n, 2) int Vertex indices which correspond to face_adjacency """# this value is calculated as a byproduct of the face adjacency_=self.face_adjacencyreturnself._cache["face_adjacency_edges"]@cache_decoratordefface_adjacency_edges_tree(self)->cKDTree:""" A KDTree for mapping edges back face adjacency index. Returns ------------ tree : scipy.spatial.cKDTree Tree when queried with SORTED edges will return their index in mesh.face_adjacency """returncKDTree(self.face_adjacency_edges)@cache_decoratordefface_adjacency_angles(self)->NDArray[float64]:""" Return the angle between adjacent faces Returns -------- adjacency_angle : (n, ) float Angle between adjacent faces Each value corresponds with self.face_adjacency """# get pairs of unit vectors for adjacent facespairs=self.face_normals[self.face_adjacency]# find the angle between the pairs of vectorsangles=geometry.vector_angle(pairs)returnangles@cache_decoratordefface_adjacency_projections(self)->NDArray[float64]:""" The projection of the non-shared vertex of a triangle onto its adjacent face Returns ---------- projections : (len(self.face_adjacency), ) float Dot product of vertex onto plane of adjacent triangle. """projections=convex.adjacency_projections(self)returnprojections@cache_decoratordefface_adjacency_convex(self)->NDArray[np.bool_]:""" Return faces which are adjacent and locally convex. What this means is that given faces A and B, the one vertex in B that is not shared with A, projected onto the plane of A has a projection that is zero or negative. Returns ---------- are_convex : (len(self.face_adjacency), ) bool Face pairs that are locally convex """returnself.face_adjacency_projections<tol.merge@cache_decoratordefface_adjacency_unshared(self)->NDArray[int64]:""" Return the vertex index of the two vertices not in the shared edge between two adjacent faces Returns ----------- vid_unshared : (len(mesh.face_adjacency), 2) int Indexes of mesh.vertices """returngraph.face_adjacency_unshared(self)@cache_decoratordefface_adjacency_radius(self)->NDArray[float64]:""" The approximate radius of a cylinder that fits inside adjacent faces. Returns ------------ radii : (len(self.face_adjacency), ) float Approximate radius formed by triangle pair """radii,self._cache["face_adjacency_span"]=graph.face_adjacency_radius(mesh=self)returnradii@cache_decoratordefface_adjacency_span(self)->NDArray[float64]:""" The approximate perpendicular projection of the non-shared vertices in a pair of adjacent faces onto the shared edge of the two faces. Returns ------------ span : (len(self.face_adjacency), ) float Approximate span between the non-shared vertices """_=self.face_adjacency_radiusreturnself._cache["face_adjacency_span"]@cache_decoratordefintegral_mean_curvature(self)->float64:""" The integral mean curvature, or the surface integral of the mean curvature. Returns --------- area : float Integral mean curvature of mesh """edges_length=np.linalg.norm(np.subtract(*self.vertices[self.face_adjacency_edges.T]),axis=1)return(self.face_adjacency_angles*edges_length).sum()*0.5@cache_decoratordefvertex_adjacency_graph(self)->Graph:""" Returns a networkx graph representing the vertices and their connections in the mesh. Returns --------- graph: networkx.Graph Graph representing vertices and edges between them where vertices are nodes and edges are edges Examples ---------- This is useful for getting nearby vertices for a given vertex, potentially for some simple smoothing techniques. mesh = trimesh.primitives.Box() graph = mesh.vertex_adjacency_graph graph.neighbors(0) > [1, 2, 3, 4] """returngraph.vertex_adjacency_graph(mesh=self)@cache_decoratordefvertex_neighbors(self)->List[List[int64]]:""" The vertex neighbors of each vertex of the mesh, determined from the cached vertex_adjacency_graph, if already existent. Returns ---------- vertex_neighbors : (len(self.vertices), ) int Represents immediate neighbors of each vertex along the edge of a triangle Examples ---------- This is useful for getting nearby vertices for a given vertex, potentially for some simple smoothing techniques. >>> mesh = trimesh.primitives.Box() >>> mesh.vertex_neighbors[0] [1, 2, 3, 4] """returngraph.neighbors(edges=self.edges_unique,max_index=len(self.vertices))@cache_decoratordefis_winding_consistent(self)->bool:""" Does the mesh have consistent winding or not. A mesh with consistent winding has each shared edge going in an opposite direction from the other in the pair. Returns -------- consistent : bool Is winding is consistent or not """ifself.is_empty:returnFalse# consistent winding check is populated into the cache by is_watertight_=self.is_watertightreturnself._cache["is_winding_consistent"]@cache_decoratordefis_watertight(self)->bool:""" Check if a mesh is watertight by making sure every edge is included in two faces. Returns ---------- is_watertight : bool Is mesh watertight or not """ifself.is_empty:returnFalsewatertight,winding=graph.is_watertight(edges=self.edges,edges_sorted=self.edges_sorted)self._cache["is_winding_consistent"]=windingreturnwatertight@cache_decoratordefis_volume(self)->bool:""" Check if a mesh has all the properties required to represent a valid volume, rather than just a surface. These properties include being watertight, having consistent winding and outward facing normals. Returns --------- valid : bool Does the mesh represent a volume """valid=bool(self.is_watertightandself.is_winding_consistentandnp.isfinite(self.center_mass).all()andself.volume>0.0)returnvalid@propertydefis_empty(self)->bool:""" Does the current mesh have data defined. Returns -------- empty : bool If True, no data is set on the current mesh """returnself._data.is_empty()@cache_decoratordefis_convex(self)->bool:""" Check if a mesh is convex or not. Returns ---------- is_convex: bool Is mesh convex or not """ifself.is_empty:returnFalseis_convex=bool(convex.is_convex(self))returnis_convex@cache_decoratordefkdtree(self)->cKDTree:""" Return a scipy.spatial.cKDTree of the vertices of the mesh. Not cached as this lead to observed memory issues and segfaults. Returns --------- tree : scipy.spatial.cKDTree Contains mesh.vertices """returncKDTree(self.vertices.view(np.ndarray))
[docs]defremove_degenerate_faces(self,height:float=tol.merge)->None:""" DERECATED MARCH 2024 REPLACE WITH: `self.update_faces(self.nondegenerate_faces(height=height))` """warnings.warn("`remove_degenerate_faces` is deprecated "+"and will be removed in March 2024 replace with "+"`self.update_faces(self.nondegenerate_faces(height=height))`",category=DeprecationWarning,stacklevel=2,)self.update_faces(self.nondegenerate_faces(height=height))
[docs]defnondegenerate_faces(self,height:float=tol.merge)->NDArray[np.bool_]:""" Identify degenerate faces (faces without 3 unique vertex indices) in the current mesh. Usage example for removing them: `mesh.update_faces(mesh.nondegenerate_faces())` If a height is specified, it will identify any face with a 2D oriented bounding box with one edge shorter than that height. If not specified, it will identify any face with a zero normal. Parameters ------------ height : float If specified identifies faces with an oriented bounding box shorter than this on one side. Returns ------------- nondegenerate : (len(self.faces), ) bool Mask that can be used to remove faces """returntriangles.nondegenerate(self.triangles,areas=self.area_faces,height=height)
@cache_decoratordeffacets(self)->List[NDArray[int64]]:""" Return a list of face indices for coplanar adjacent faces. Returns --------- facets : (n, ) sequence of (m, ) int Groups of indexes of self.faces """facets=graph.facets(self)returnfacets@cache_decoratordeffacets_area(self)->NDArray[float64]:""" Return an array containing the area of each facet. Returns --------- area : (len(self.facets), ) float Total area of each facet (group of faces) """# avoid thrashing the cache inside a looparea_faces=self.area_faces# sum the area of each group of faces represented by facets# use native python sum in tight loop as opposed to array.sum()# as in this case the lower function call overhead of# native sum provides roughly a 50% speedupareas=np.array([sum(area_faces[i])foriinself.facets],dtype=float64)returnareas@cache_decoratordeffacets_normal(self)->NDArray[float64]:""" Return the normal of each facet Returns --------- normals: (len(self.facets), 3) float A unit normal vector for each facet """iflen(self.facets)==0:returnnp.array([])area_faces=self.area_faces# the face index of the largest face in each facetindex=np.array([i[area_faces[i].argmax()]foriinself.facets])# (n, 3) float, unit normal vectors of facet planenormals=self.face_normals[index]# (n, 3) float, points on facet planeorigins=self.vertices[self.faces[:,0][index]]# save origins in cacheself._cache["facets_origin"]=originsreturnnormals@cache_decoratordeffacets_origin(self)->NDArray[float64]:""" Return a point on the facet plane. Returns ------------ origins : (len(self.facets), 3) float A point on each facet plane """_=self.facets_normalreturnself._cache["facets_origin"]@cache_decoratordeffacets_boundary(self)->List[NDArray[int64]]:""" Return the edges which represent the boundary of each facet Returns --------- edges_boundary : sequence of (n, 2) int Indices of self.vertices """# make each row correspond to a single faceedges=self.edges_sorted.reshape((-1,6))# get the edges for each facetedges_facet=[edges[i].reshape((-1,2))foriinself.facets]edges_boundary=[i[grouping.group_rows(i,require_count=1)]foriinedges_facet]returnedges_boundary@cache_decoratordeffacets_on_hull(self)->NDArray[np.bool_]:""" Find which facets of the mesh are on the convex hull. Returns --------- on_hull : (len(mesh.facets), ) bool is A facet on the meshes convex hull or not """# if no facets exit earlyiflen(self.facets)==0:returnnp.array([],dtype=bool)# facets plane, origin and normalnormals=self.facets_normalorigins=self.facets_origin# (n, 3) convex hull verticesconvex=self.convex_hull.vertices.view(np.ndarray).copy()# boolean mask for which facets are on convex hullon_hull=np.zeros(len(self.facets),dtype=bool)fori,normal,origininzip(range(len(normals)),normals,origins):# a facet plane is on the convex hull if every vertex# of the convex hull is behind that plane# which we are checking with dot productsdot=np.dot(normal,(convex-origin).T)on_hull[i]=(dot<tol.merge).all()returnon_hull
[docs]deffix_normals(self,multibody:Optional[bool]=None)->None:""" Find and fix problems with self.face_normals and self.faces winding direction. For face normals ensure that vectors are consistently pointed outwards, and that self.faces is wound in the correct direction for all connected components. Parameters ------------- multibody : None or bool Fix normals across multiple bodies if None automatically pick from body_count """ifmultibodyisNone:multibody=self.body_count>1repair.fix_normals(self,multibody=multibody)
[docs]deffill_holes(self)->bool:""" Fill single triangle and single quad holes in the current mesh. Returns ---------- watertight : bool Is the mesh watertight after the function completes """returnrepair.fill_holes(self)
[docs]defregister(self,other:Union[Geometry3D,NDArray],**kwargs):""" Align a mesh with another mesh or a PointCloud using the principal axes of inertia as a starting point which is refined by iterative closest point. Parameters ------------ other : trimesh.Trimesh or (n, 3) float Mesh or points in space samples : int Number of samples from mesh surface to align icp_first : int How many ICP iterations for the 9 possible combinations of icp_final : int How many ICP itertations for the closest candidate from the wider search Returns ----------- mesh_to_other : (4, 4) float Transform to align mesh to the other object cost : float Average square distance per point """mesh_to_other,cost=registration.mesh_other(mesh=self,other=other,**kwargs)returnmesh_to_other,cost
[docs]defcompute_stable_poses(self,center_mass:Optional[NDArray[float64]]=None,sigma:float=0.0,n_samples:int=1,threshold:float=0.0,):""" Computes stable orientations of a mesh and their quasi-static probabilities. This method samples the location of the center of mass from a multivariate gaussian (mean at com, cov equal to identity times sigma) over n_samples. For each sample, it computes the stable resting poses of the mesh on a a planar workspace and evaluates the probabilities of landing in each pose if the object is dropped onto the table randomly. This method returns the 4x4 homogeneous transform matrices that place the shape against the planar surface with the z-axis pointing upwards and a list of the probabilities for each pose. The transforms and probabilities that are returned are sorted, with the most probable pose first. Parameters ------------ center_mass : (3, ) float The object center of mass (if None, this method assumes uniform density and watertightness and computes a center of mass explicitly) sigma : float The covariance for the multivariate gaussian used to sample center of mass locations n_samples : int The number of samples of the center of mass location threshold : float The probability value at which to threshold returned stable poses Returns ------- transforms : (n, 4, 4) float The homogeneous matrices that transform the object to rest in a stable pose, with the new z-axis pointing upwards from the table and the object just touching the table. probs : (n, ) float A probability ranging from 0.0 to 1.0 for each pose """returnposes.compute_stable_poses(mesh=self,center_mass=center_mass,sigma=sigma,n_samples=n_samples,threshold=threshold,)
[docs]defsubdivide(self,face_index:Optional[ArrayLike]=None)->"Trimesh":""" Subdivide a mesh with each subdivided face replaced with four smaller faces. Will return a copy of current mesh with subdivided faces. Parameters ------------ face_index : (m, ) int or None If None all faces of mesh will be subdivided If (m, ) int array of indices: only specified faces will be subdivided. Note that in this case the mesh will generally no longer be manifold, as the additional vertex on the midpoint will not be used by the adjacent faces to the faces specified, and an additional postprocessing step will be required to make resulting mesh watertight """visual=Noneifhasattr(self.visual,"uv")andnp.shape(self.visual.uv)==(len(self.vertices),2,):# uv coords divided along with verticesvertices,faces,attr=remesh.subdivide(vertices=np.hstack((self.vertices,self.visual.uv)),faces=self.faces,face_index=face_index,vertex_attributes=self.vertex_attributes,)# get a copy of the current visualsvisual=self.visual.copy()# separate uv coords and verticesvertices,visual.uv=vertices[:,:3],vertices[:,3:]else:# perform the subdivision with vertex attributesvertices,faces,attr=remesh.subdivide(vertices=self.vertices,faces=self.faces,face_index=face_index,vertex_attributes=self.vertex_attributes,)# create a new meshresult=Trimesh(vertices=vertices,faces=faces,visual=visual,vertex_attributes=attr,process=False,)returnresult
[docs]defsubdivide_to_size(self,max_edge,max_iter=10,return_index=False):""" Subdivide a mesh until every edge is shorter than a specified length. Will return a triangle soup, not a nicely structured mesh. Parameters ------------ max_edge : float Maximum length of any edge in the result max_iter : int The maximum number of times to run subdivision return_index : bool If True, return index of original face for new faces """# subdivide vertex attributesvisual=Noneifhasattr(self.visual,"uv")andnp.shape(self.visual.uv)==(len(self.vertices),2,):# uv coords divided along with verticesvertices_faces=remesh.subdivide_to_size(vertices=np.hstack((self.vertices,self.visual.uv)),faces=self.faces,max_edge=max_edge,max_iter=max_iter,return_index=return_index,)# unpack resultifreturn_index:vertices,faces,final_index=vertices_faceselse:vertices,faces=vertices_faces# get a copy of the current visualsvisual=self.visual.copy()# separate uv coords and verticesvertices,visual.uv=vertices[:,:3],vertices[:,3:]else:# uv coords divided along with verticesvertices_faces=remesh.subdivide_to_size(vertices=self.vertices,faces=self.faces,max_edge=max_edge,max_iter=max_iter,return_index=return_index,)# unpack resultifreturn_index:vertices,faces,final_index=vertices_faceselse:vertices,faces=vertices_faces# create a new meshresult=Trimesh(vertices=vertices,faces=faces,visual=visual,process=False)ifreturn_index:returnresult,final_indexreturnresult
[docs]defsubdivide_loop(self,iterations=None):""" Subdivide a mesh by dividing each triangle into four triangles and approximating their smoothed surface using loop subdivision. Loop subdivision often looks better on triangular meshes than catmul-clark, which operates primarily on quads. Parameters ------------ iterations : int Number of iterations to run subdivision. multibody : bool If True will try to subdivide for each submesh """# perform subdivision for one meshnew_vertices,new_faces=remesh.subdivide_loop(vertices=self.vertices,faces=self.faces,iterations=iterations)# create new meshresult=Trimesh(vertices=new_vertices,faces=new_faces,process=False)returnresult
[docs]defsmoothed(self,**kwargs):""" DEPRECATED: use `mesh.smooth_shaded` or `trimesh.graph.smooth_shade(mesh)` """warnings.warn("`mesh.smoothed()` is deprecated and will be removed in March 2024: "+"use `mesh.smooth_shaded` or `trimesh.graph.smooth_shade(mesh)`",category=DeprecationWarning,stacklevel=2,)# run smoothingreturnself.smooth_shaded
@propertydefsmooth_shaded(self):""" Smooth shading in OpenGL relies on which vertices are shared, this function will disconnect regions above an angle threshold and return a non-watertight version which will look better in an OpenGL rendering context. If you would like to use non-default arguments see `graph.smooth_shade`. Returns --------- smooth_shaded : trimesh.Trimesh Non watertight version of current mesh. """# key this also by the visual properties# but store it in the mesh cacheself.visual._verify_hash()cache=self.visual._cache# needs to be dumped whenever visual or mesh changeskey=f"smooth_shaded_{hash(self.visual)}_{hash(self)}"ifkeyincache:returncache[key]smooth=graph.smooth_shade(self)# store it in the mesh cache which dumps when vertices changecache[key]=smoothreturnsmooth@propertydefvisual(self):""" Get the stored visuals for the current mesh. Returns ------------- visual : ColorVisuals or TextureVisuals Contains visual information about the mesh """ifhasattr(self,"_visual"):returnself._visualreturnNone@visual.setterdefvisual(self,value):""" When setting a visual object, always make sure that `visual.mesh` points back to the source mesh. Parameters -------------- visual : ColorVisuals or TextureVisuals Contains visual information about the mesh """value.mesh=selfself._visual=value
[docs]defsection(self,plane_normal:ArrayLike,plane_origin:ArrayLike,**kwargs)->Optional[Path3D]:""" Returns a 3D cross section of the current mesh and a plane defined by origin and normal. Parameters ------------ plane_normal : (3,) float Normal vector of section plane. plane_origin : (3, ) float Point on the cross section plane. Returns --------- intersections Curve of intersection or None if it was not hit by plane. """# turn line segments into Path2D/Path3D objectsfrom.path.exchange.miscimportlines_to_pathfrom.path.pathimportPath3D# return a single cross section in 3Dlines,face_index=intersections.mesh_plane(mesh=self,plane_normal=plane_normal,plane_origin=plane_origin,return_faces=True,**kwargs,)# if the section didn't hit the mesh return Noneiflen(lines)==0:returnNone# otherwise load the line segments into the keyword arguments# for a Path3D object.path=lines_to_path(lines)# add the face index info into metadata# path.metadata["face_index"] = face_indexreturnPath3D(**path)
[docs]defsection_multiplane(self,plane_origin:ArrayLike,plane_normal:ArrayLike,heights:ArrayLike,)->List[Optional[Path2D]]:""" Return multiple parallel cross sections of the current mesh in 2D. Parameters ------------ plane_origin : (3, ) float Point on the cross section plane plane_normal : (3) float Normal vector of section plane heights : (n, ) float Each section is offset by height along the plane normal. Returns --------- paths : (n, ) Path2D or None 2D cross sections at specified heights. path.metadata['to_3D'] contains transform to return 2D section back into 3D space. """# turn line segments into Path2D/Path3D objectsfrom.exchange.loadimportload_path# do a multiplane intersectionlines,transforms,faces=intersections.mesh_multiplane(mesh=self,plane_normal=plane_normal,plane_origin=plane_origin,heights=heights,)# turn the line segments into Path2D objectspaths=[None]*len(lines)fori,f,segments,Tinzip(range(len(lines)),faces,lines,transforms):iflen(segments)>0:paths[i]=load_path(segments,metadata={"to_3D":T,"face_index":f})returnpaths
[docs]defslice_plane(self,plane_origin,plane_normal,cap=False,face_index=None,cached_dots=None,**kwargs,):""" Slice the mesh with a plane, returning a new mesh that is the portion of the original mesh to the positive normal side of the plane plane_origin : (3,) float Point on plane to intersect with mesh plane_normal : (3,) float Normal vector of plane to intersect with mesh cap : bool If True, cap the result with a triangulated polygon face_index : ((m,) int) Indexes of mesh.faces to slice. When no mask is provided, the default is to slice all faces. cached_dots : (n, 3) float If an external function has stored dot products pass them here to avoid recomputing Returns --------- new_mesh: trimesh.Trimesh or None Subset of current mesh that intersects the half plane to the positive normal side of the plane """# return a new meshnew_mesh=intersections.slice_mesh_plane(mesh=self,plane_normal=plane_normal,plane_origin=plane_origin,cap=cap,face_index=face_index,cached_dots=cached_dots,**kwargs,)returnnew_mesh
[docs]defunwrap(self,image=None):""" Returns a Trimesh object equivalent to the current mesh where the vertices have been assigned uv texture coordinates. Vertices may be split into as many as necessary by the unwrapping algorithm, depending on how many uv maps they appear in. Requires `pip install xatlas` Parameters ------------ image : None or PIL.Image Image to assign to the material Returns -------- unwrapped : trimesh.Trimesh Mesh with unwrapped uv coordinates """importxatlasvmap,faces,uv=xatlas.parametrize(self.vertices,self.faces)result=Trimesh(vertices=self.vertices[vmap],faces=faces,visual=TextureVisuals(uv=uv,image=image),process=False,)# run additional checks for unwrappingiftol.strict:# check the export object to make sure we didn't# move the indices around on creationassertnp.allclose(result.visual.uv,uv)assertnp.allclose(result.faces,faces)assertnp.allclose(result.vertices,self.vertices[vmap])# check to make sure indices are still the# same order after we've exported to OBJexport=result.export(file_type="obj")uv_recon=np.array([L[3:].split()forLinstr.splitlines(export)ifL.startswith("vt ")],dtype=float64,)assertnp.allclose(uv_recon,uv)v_recon=np.array([L[2:].split()forLinstr.splitlines(export)ifL.startswith("v ")],dtype=float64,)assertnp.allclose(v_recon,self.vertices[vmap])returnresult
@cache_decoratordefconvex_hull(self)->"Trimesh":""" Returns a Trimesh object representing the convex hull of the current mesh. Returns -------- convex : trimesh.Trimesh Mesh of convex hull of current mesh """returnconvex.convex_hull(self)
[docs]defsample(self,count:int,return_index:bool=False,face_weight:Optional[NDArray[float64]]=None,):""" Return random samples distributed across the surface of the mesh Parameters ------------ count : int Number of points to sample return_index : bool If True will also return the index of which face each sample was taken from. face_weight : None or len(mesh.faces) float Weight faces by a factor other than face area. If None will be the same as face_weight=mesh.area Returns --------- samples : (count, 3) float Points on surface of mesh face_index : (count, ) int Index of self.faces """samples,index=sample.sample_surface(mesh=self,count=count,face_weight=face_weight)ifreturn_index:returnsamples,indexreturnsamples
[docs]defremove_unreferenced_vertices(self)->None:""" Remove all vertices in the current mesh which are not referenced by a face. """referenced=np.zeros(len(self.vertices),dtype=bool)referenced[self.faces]=Trueinverse=np.zeros(len(self.vertices),dtype=int64)inverse[referenced]=np.arange(referenced.sum())self.update_vertices(mask=referenced,inverse=inverse)
[docs]defunmerge_vertices(self)->None:""" Removes all face references so that every face contains three unique vertex indices and no faces are adjacent. """# new faces are incrementing so every vertex is uniquefaces=np.arange(len(self.faces)*3,dtype=int64).reshape((-1,3))# use update_vertices to apply mask to# all properties that are per-vertexself.update_vertices(self.faces.reshape(-1))# set faces to incrementing indexesself.faces=faces# keep face normals as the haven't changedself._cache.clear(exclude=["face_normals"])
[docs]defapply_transform(self,matrix:ArrayLike)->"Trimesh":""" Transform mesh by a homogeneous transformation matrix. Does the bookkeeping to avoid recomputing things so this function should be used rather than directly modifying self.vertices if possible. Parameters ------------ matrix : (4, 4) float Homogeneous transformation matrix """# get c-order float64 matrixmatrix=np.asanyarray(matrix,order="C",dtype=float64)# only support homogeneous transformationsifmatrix.shape!=(4,4):raiseValueError("Transformation matrix must be (4, 4)!")# exit early if we've been passed an identity matrix# np.allclose is surprisingly slow so do this testelifutil.allclose(matrix,_IDENTITY4,1e-8):returnself# new vertex positionsnew_vertices=transformations.transform_points(self.vertices,matrix=matrix)# check to see if the matrix has rotation# rather than just translationhas_rotation=notutil.allclose(matrix[:3,:3],_IDENTITY3,atol=1e-6)# transform overridden center of massif"center_mass"inself._data:center_mass=[self._data["center_mass"]]self.center_mass=transformations.transform_points(center_mass,matrix,)[0]# preserve face normals if we have them storedifhas_rotationand"face_normals"inself._cache:# transform face normals by rotation componentself._cache.cache["face_normals"]=util.unitize(transformations.transform_points(self.face_normals,matrix=matrix,translate=False))# preserve vertex normals if we have them storedifhas_rotationand"vertex_normals"inself._cache:self._cache.cache["vertex_normals"]=util.unitize(transformations.transform_points(self.vertex_normals,matrix=matrix,translate=False))# if transformation flips winding of trianglesifhas_rotationandtransformations.flips_winding(matrix):log.debug("transform flips winding")# fliplr will make array non C contiguous# which will cause hashes to be more# expensive than necessary so wrapself.faces=np.ascontiguousarray(np.fliplr(self.faces))# assign the new valuesself.vertices=new_vertices# preserve normals and topology in cache# while dumping everything elseself._cache.clear(exclude={"face_normals",# transformed by us"vertex_normals",# also transformed by us"face_adjacency",# topological"face_adjacency_edges","face_adjacency_unshared","edges","edges_face","edges_sorted","edges_unique","edges_unique_idx","edges_unique_inverse","edges_sparse","body_count","faces_unique_edges","euler_number",})# set the cache ID with the current hash valueself._cache.id_set()returnself
[docs]defvoxelized(self,pitch,method="subdivide",**kwargs):""" Return a VoxelGrid object representing the current mesh discretized into voxels at the specified pitch Parameters ------------ pitch : float The edge length of a single voxel method: implementation key. See `trimesh.voxel.creation.voxelizers` **kwargs: additional kwargs passed to the specified implementation. Returns ---------- voxelized : VoxelGrid object Representing the current mesh """from.voxelimportcreationreturncreation.voxelize(mesh=self,pitch=pitch,method=method,**kwargs)
[docs]defsimplify_quadric_decimation(self,percent:Optional[Floating]=None,face_count:Optional[Integer]=None,aggression:Optional[Integer]=None,)->"Trimesh":""" A thin wrapper around `pip install fast-simplification`. Parameters ----------- percent A number between 0.0 and 1.0 for how much face_count Target number of faces desired in the resulting mesh. aggression An integer between `0` and `10`, the scale being roughly `0` is "slow and good" and `10` being "fast and bad." Returns --------- simple : trimesh.Trimesh Simplified version of mesh. """fromfast_simplificationimportsimplify# create keyword arguments as dict so we can filter out `None`# values as the C wrapper as of writing is not happy with `None`# and requires they be omitted from the constructorkwargs={"target_count":face_count,"target_reduction":percent,"agg":aggression,}# todo : one could take the `return_collapses=True` array and use it to# apply the same simplification to the visual infovertices,faces=simplify(points=self.vertices.view(np.ndarray),triangles=self.faces.view(np.ndarray),**{k:vfork,vinkwargs.items()ifvisnotNone},)returnTrimesh(vertices=vertices,faces=faces)
[docs]defoutline(self,face_ids:Optional[NDArray[int64]]=None,**kwargs)->Path3D:""" Given a list of face indexes find the outline of those faces and return it as a Path3D. The outline is defined here as every edge which is only included by a single triangle. Note that this implies a non-watertight mesh as the outline of a watertight mesh is an empty path. Parameters ------------ face_ids : (n, ) int Indices to compute the outline of. If None, outline of full mesh will be computed. **kwargs: passed to Path3D constructor Returns ---------- path : Path3D Curve in 3D of the outline """from.path.exchange.miscimportfaces_to_pathreturnPath3D(**faces_to_path(self,face_ids,**kwargs))
[docs]defprojected(self,normal,**kwargs)->Path2D:""" Project a mesh onto a plane and then extract the polygon that outlines the mesh projection on that plane. Parameters ---------- mesh : trimesh.Trimesh Source geometry check : bool If True make sure is flat normal : (3,) float Normal to extract flat pattern along origin : None or (3,) float Origin of plane to project mesh onto pad : float Proportion to pad polygons by before unioning and then de-padding result by to avoid zero-width gaps. tol_dot : float Tolerance for discarding on-edge triangles. max_regions : int Raise an exception if the mesh has more than this number of disconnected regions to fail quickly before unioning. Returns ---------- projected : trimesh.path.Path2D Outline of source mesh """from.exchange.loadimportload_pathfrom.pathimportPath2Dfrom.path.polygonsimportprojectedprojection=projected(mesh=self,normal=normal,**kwargs)ifprojectionisNone:returnPath2D()returnload_path(projection)
@cache_decoratordefarea(self)->float64:""" Summed area of all triangles in the current mesh. Returns --------- area : float Surface area of mesh """area=self.area_faces.sum()returnarea@cache_decoratordefarea_faces(self)->NDArray[float64]:""" The area of each face in the mesh. Returns --------- area_faces : (n, ) float Area of each face """returntriangles.area(crosses=self.triangles_cross)@cache_decoratordefmass_properties(self)->MassProperties:""" Returns the mass properties of the current mesh. Assumes uniform density, and result is probably garbage if mesh isn't watertight. Returns ---------- properties : dict With keys: 'volume' : in global units^3 'mass' : From specified density 'density' : Included again for convenience (same as kwarg density) 'inertia' : Taken at the center of mass and aligned with global coordinate system 'center_mass' : Center of mass location, in global coordinate system """# if the density or center of mass was overridden they will be put into datadensity=self._data.data.get("density",None)center_mass=self._data.data.get("center_mass",None)returntriangles.mass_properties(triangles=self.triangles,crosses=self.triangles_cross,density=density,center_mass=center_mass,skip_inertia=False,)
[docs]definvert(self)->None:""" Invert the mesh in-place by reversing the winding of every face and negating normals without dumping the cache. Alters `self.faces` by reversing columns, and negating `self.face_normals` and `self.vertex_normals`. """withself._cache:if"face_normals"inself._cache:self.face_normals=self._cache["face_normals"]*-1.0if"vertex_normals"inself._cache:self.vertex_normals=self._cache["vertex_normals"]*-1.0# fliplr makes array non-contiguous so cache checks slowself.faces=np.ascontiguousarray(np.fliplr(self.faces))# save our normalsself._cache.clear(exclude=["face_normals","vertex_normals"])
[docs]defscene(self,**kwargs)->Scene:""" Returns a Scene object containing the current mesh. Returns --------- scene : trimesh.scene.scene.Scene Contains just the current mesh """returnScene(self,**kwargs)
[docs]defshow(self,**kwargs):""" Render the mesh in an opengl window. Requires pyglet. Parameters ------------ smooth : bool Run smooth shading on mesh or not, large meshes will be slow Returns ----------- scene : trimesh.scene.Scene Scene with current mesh in it """scene=self.scene()returnscene.show(**kwargs)
[docs]defsubmesh(self,faces_sequence:Sequence[ArrayLike],**kwargs)->Union["Trimesh",List["Trimesh"]]:""" Return a subset of the mesh. Parameters ------------ faces_sequence : sequence (m, ) int Face indices of mesh only_watertight : bool Only return submeshes which are watertight append : bool Return a single mesh which has the faces appended. if this flag is set, only_watertight is ignored Returns --------- submesh : Trimesh or (n,) Trimesh Single mesh if `append` or list of submeshes """returnutil.submesh(mesh=self,faces_sequence=faces_sequence,**kwargs)
@cache_decoratordefidentifier(self)->NDArray[float64]:""" Return a float vector which is unique to the mesh and is robust to rotation and translation. Returns ----------- identifier : (7,) float Identifying properties of the current mesh """returncomparison.identifier_simple(self)@cache_decoratordefidentifier_hash(self)->str:""" A hash of the rotation invariant identifier vector. Returns --------- hashed : str Hex string of the SHA256 hash from the identifier vector at hand-tuned sigfigs. """returncomparison.identifier_hash(self.identifier)
[docs]defexport(self,file_obj=None,file_type:Optional[str]=None,**kwargs,):""" Export the current mesh to a file object. If file_obj is a filename, file will be written there. Supported formats are stl, off, ply, collada, json, dict, glb, dict64, msgpack. Parameters ------------ file_obj : open writeable file object str, file name where to save the mesh None, return the export blob file_type : str Which file type to export as, if `file_name` is passed this is not required. """returnexport_mesh(mesh=self,file_obj=file_obj,file_type=file_type,**kwargs)
[docs]defto_dict(self)->Dict[str,Union[str,List[List[float]],List[List[int]]]]:""" Return a dictionary representation of the current mesh with keys that can be used as the kwargs for the Trimesh constructor and matches the schema in: `trimesh/resources/schema/primitive/trimesh.schema.json` Returns ---------- result : dict Matches schema and Trimesh constructor. """return{"kind":"trimesh","vertices":self.vertices.tolist(),"faces":self.faces.tolist(),}
[docs]defconvex_decomposition(self,**kwargs)->List["Trimesh"]:""" Compute an approximate convex decomposition of a mesh using `pip install pyVHACD`. Returns ------- meshes List of convex meshes that approximate the original **kwargs : VHACD keyword arguments """return[Trimesh(**kwargs)forkwargsindecomposition.convex_decomposition(self,**kwargs)]
[docs]defunion(self,other:Union["Trimesh",Sequence["Trimesh"]],engine:Optional[str]=None,check_volume:bool=True,**kwargs,)->"Trimesh":""" Boolean union between this mesh and other meshes. Parameters ------------ other : Trimesh or (n, ) Trimesh Other meshes to union engine Which backend to use, the default recommendation is: `pip install manifold3d`. check_volume Raise an error if not all meshes are watertight positive volumes. Advanced users may want to ignore this check as it is expensive. kwargs Passed through to the `engine`. Returns --------- union : trimesh.Trimesh Union of self and other Trimesh objects """returnboolean.union(meshes=util.chain(self,other),engine=engine,check_volume=check_volume,**kwargs,)
[docs]defdifference(self,other:Union["Trimesh",Sequence["Trimesh"]],engine:Optional[str]=None,check_volume:bool=True,**kwargs,)->"Trimesh":""" Boolean difference between this mesh and other meshes. Parameters ------------ other One or more meshes to difference with the current mesh. engine Which backend to use, the default recommendation is: `pip install manifold3d`. check_volume Raise an error if not all meshes are watertight positive volumes. Advanced users may want to ignore this check as it is expensive. kwargs Passed through to the `engine`. Returns --------- difference : trimesh.Trimesh Difference between self and other Trimesh objects """returnboolean.difference(meshes=util.chain(self,other),engine=engine,check_volume=check_volume,**kwargs,)
[docs]defintersection(self,other:Union["Trimesh",Sequence["Trimesh"]],engine:Optional[str]=None,check_volume:bool=True,**kwargs,)->"Trimesh":""" Boolean intersection between this mesh and other meshes. Parameters ------------ other : trimesh.Trimesh, or list of trimesh.Trimesh objects Meshes to calculate intersections with engine Which backend to use, the default recommendation is: `pip install manifold3d`. check_volume Raise an error if not all meshes are watertight positive volumes. Advanced users may want to ignore this check as it is expensive. kwargs Passed through to the `engine`. Returns --------- intersection : trimesh.Trimesh Mesh of the volume contained by all passed meshes """returnboolean.intersection(meshes=util.chain(self,other),engine=engine,check_volume=check_volume,**kwargs,)
[docs]defcontains(self,points:ArrayLike)->NDArray[np.bool_]:""" Given an array of points determine whether or not they are inside the mesh. This raises an error if called on a non-watertight mesh. Parameters ------------ points : (n, 3) float Points in cartesian space Returns --------- contains : (n, ) bool Whether or not each point is inside the mesh """returnself.ray.contains_points(points)
@cache_decoratordefface_angles(self)->NDArray[float64]:""" Returns the angle at each vertex of a face. Returns -------- angles : (len(self.faces), 3) float Angle at each vertex of a face """returntriangles.angles(self.triangles)@cache_decoratordefface_angles_sparse(self)->coo_matrix:""" A sparse matrix representation of the face angles. Returns ---------- sparse : scipy.sparse.coo_matrix Float sparse matrix with with shape: (len(self.vertices), len(self.faces)) """angles=curvature.face_angles_sparse(self)returnangles@cache_decoratordefvertex_defects(self)->NDArray[float64]:""" Return the vertex defects, or (2*pi) minus the sum of the angles of every face that includes that vertex. If a vertex is only included by coplanar triangles, this will be zero. For convex regions this is positive, and concave negative. Returns -------- vertex_defect : (len(self.vertices), ) float Vertex defect at the every vertex """defects=curvature.vertex_defects(self)returndefects@cache_decoratordefvertex_degree(self)->NDArray[int64]:""" Return the number of faces each vertex is included in. Returns ---------- degree : (len(self.vertices), ) int Number of faces each vertex is included in """# get degree through sparse matrixdegree=np.array(self.faces_sparse.sum(axis=1)).flatten()returndegree@cache_decoratordefface_adjacency_tree(self)->Index:""" An R-tree of face adjacencies. Returns -------- tree Where each edge in self.face_adjacency has a rectangular cell """# the (n,6) interleaved bounding box for every line segmentreturnutil.bounds_tree(np.column_stack((self.vertices[self.face_adjacency_edges].min(axis=1),self.vertices[self.face_adjacency_edges].max(axis=1),)))
[docs]defcopy(self,include_cache:bool=False,include_visual:bool=True)->"Trimesh":""" Safely return a copy of the current mesh. By default, copied meshes will have emptied cache to avoid memory issues and so may be slow on initial operations until caches are regenerated. Current object will *never* have its cache cleared. Parameters ------------ include_cache : bool If True, will shallow copy cached data to new mesh Returns --------- copied : trimesh.Trimesh Copy of current mesh """# start with an empty meshcopied=Trimesh()# always deepcopy vertex and face datacopied._data.data=copy.deepcopy(self._data.data)ifinclude_visual:# copy visual informationcopied.visual=self.visual.copy()# get metadatacopied.metadata=copy.deepcopy(self.metadata)# make sure cache ID is set initiallycopied._cache.verify()ifinclude_cache:# shallow copy cached items into the new cache# since the data didn't change here when the# data in the new mesh is changed these items# will be dumped in the new mesh but preserved# in the original meshcopied._cache.cache.update(self._cache.cache)returncopied
def__deepcopy__(self,*args)->"Trimesh":# interpret deep copy as "get rid of cached data"returnself.copy(include_cache=False)def__copy__(self,*args)->"Trimesh":# interpret shallow copy as "keep cached data"returnself.copy(include_cache=True)
[docs]defeval_cached(self,statement:str,*args):""" Evaluate a statement and cache the result before returning. Statements are evaluated inside the Trimesh object, and Parameters ------------ statement : str Statement of valid python code *args : list Available inside statement as args[0], etc Returns ----------- result : result of running eval on statement with args Examples ----------- r = mesh.eval_cached('np.dot(self.vertices, args[0])', [0, 0, 1]) """# store this by the combined hash of statement and argshashable=[hash(statement)]hashable.extend(hash(a)forainargs)key=f"eval_cached_{hash(tuple(hashable))}"ifkeyinself._cache:returnself._cache[key]result=eval(statement)self._cache[key]=resultreturnresult
[docs]def__add__(self,other:"Trimesh")->"Trimesh":""" Concatenate the mesh with another mesh. Parameters ------------ other : trimesh.Trimesh object Mesh to be concatenated with self Returns ---------- concat : trimesh.Trimesh Mesh object of combined result """concat=util.concatenate(self,other)returnconcat