Coverage for trimesh/base.py: 93%
815 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2# trimesh
4https://github.com/mikedh/trimesh
5---------------------------------
7Library for importing, exporting and doing simple operations on triangular meshes.
8"""
10from copy import deepcopy
11from typing import Any
13import numpy as np
14from numpy import float64, int64, ndarray
16from . import (
17 boolean,
18 comparison,
19 convex,
20 curvature,
21 decomposition,
22 geometry,
23 graph,
24 grouping,
25 inertia,
26 intersections,
27 permutate,
28 poses,
29 proximity,
30 ray,
31 registration,
32 remesh,
33 repair,
34 sample,
35 transformations,
36 triangles,
37 units,
38 util,
39 visual,
40)
41from .caching import Cache, DataStore, TrackedArray, cache_decorator
42from .constants import log, tol
43from .exceptions import ExceptionWrapper
44from .exchange.export import export_mesh
45from .parent import Geometry3D
46from .scene import Scene
47from .triangles import MassProperties
48from .typed import (
49 ArrayLike,
50 BooleanEngineType,
51 Floating,
52 Integer,
53 Loadable,
54 NDArray,
55 Number,
56 Self,
57 Sequence,
58 ViewerType,
59)
60from .visual import ColorVisuals, TextureVisuals, create_visual
62try:
63 from scipy.sparse import coo_matrix
64 from scipy.spatial import cKDTree
65except BaseException as E:
66 cKDTree = ExceptionWrapper(E)
67 coo_matrix = ExceptionWrapper(E)
68try:
69 from networkx import Graph
70except BaseException as E:
71 Graph = ExceptionWrapper(E)
73try:
74 from PIL import Image
75except BaseException as E:
76 Image = ExceptionWrapper(E)
78try:
79 from rtree.index import Index
80except BaseException as E:
81 Index = ExceptionWrapper(E)
83try:
84 from .path import Path2D, Path3D
85except BaseException as E:
86 Path2D = ExceptionWrapper(E)
87 Path3D = ExceptionWrapper(E)
89# save immutable identity matrices for checks
90_IDENTITY3 = np.eye(3, dtype=np.float64)
91_IDENTITY3.flags.writeable = False
92_IDENTITY4 = np.eye(4, dtype=np.float64)
93_IDENTITY4.flags.writeable = False
96class Trimesh(Geometry3D):
97 def __init__(
98 self,
99 vertices: ArrayLike | None = None,
100 faces: ArrayLike | None = None,
101 face_normals: ArrayLike | None = None,
102 vertex_normals: ArrayLike | None = None,
103 face_colors: ArrayLike | None = None,
104 vertex_colors: ArrayLike | None = None,
105 face_attributes: dict[str, ArrayLike] | None = None,
106 vertex_attributes: dict[str, ArrayLike] | None = None,
107 metadata: dict[str, Any] | None = None,
108 process: bool = True,
109 validate: bool = False,
110 merge_tex: bool | None = None,
111 merge_norm: bool | None = None,
112 use_embree: bool = True,
113 initial_cache: dict[str, ndarray] | None = None,
114 visual: ColorVisuals | TextureVisuals | None = None,
115 **kwargs,
116 ) -> None:
117 """
118 A Trimesh object contains a triangular 3D mesh.
120 Parameters
121 ------------
122 vertices : (n, 3) float
123 Array of vertex locations
124 faces : (m, 3) or (m, 4) int
125 Array of triangular or quad faces (triangulated on load)
126 face_normals : (m, 3) float
127 Array of normal vectors corresponding to faces
128 vertex_normals : (n, 3) float
129 Array of normal vectors for vertices
130 face_colors : (n, 3|4) uint8
131 Array of colors for faces
132 vertex_colors : (n, 3|4) uint8
133 Array of colors for vertices
134 face_attributes : dict
135 Attributes corresponding to faces
136 vertex_attributes : dict
137 Attributes corresponding to vertices
138 metadata : dict
139 Any metadata about the mesh
140 process : bool
141 if True, Nan and Inf values will be removed
142 immediately and vertices will be merged
143 validate : bool
144 If True, degenerate and duplicate faces will be
145 removed immediately, and some functions will alter
146 the mesh to ensure consistent results.
147 merge_tex : bool
148 If True textured meshes with UV coordinates will
149 have vertices merged regardless of UV coordinates
150 merge_norm : bool
151 If True, meshes with vertex normals will have
152 vertices merged ignoring different normals
153 use_embree : bool
154 If True try to use pyembree raytracer.
155 If pyembree is not available it will automatically fall
156 back to a much slower rtree/numpy implementation
157 initial_cache : dict
158 A way to pass things to the cache in case expensive
159 things were calculated before creating the mesh object.
160 visual : ColorVisuals or TextureVisuals
161 Assigned to self.visual
162 """
164 # self._data stores information about the mesh which
165 # CANNOT be regenerated.
166 # in the base class all that is stored here is vertex and
167 # face information
168 # any data put into the store is converted to a TrackedArray
169 # which is a subclass of np.ndarray that provides hash and crc
170 # methods which can be used to detect changes in the array.
171 self._data = DataStore()
173 # self._cache stores information about the mesh which CAN be
174 # regenerated from self._data, but may be slow to calculate.
175 # In order to maintain consistency
176 # the cache is cleared when self._data.__hash__() changes
177 self._cache = Cache(id_function=self._data.__hash__, force_immutable=True)
178 if initial_cache is not None:
179 self._cache.update(initial_cache)
181 # check for None only to avoid warning messages in subclasses
183 # (n, 3) float array of vertices
184 self.vertices = vertices
186 # (m, 3) int of triangle faces that references self.vertices
187 self.faces = faces
189 # store per-face and per-vertex attributes which will
190 # be updated when an update_faces call is made
191 self.face_attributes = {}
192 self.vertex_attributes = {}
194 # hold visual information about the mesh (vertex and face colors)
195 if visual is None:
196 self.visual = create_visual(
197 face_colors=face_colors, vertex_colors=vertex_colors, mesh=self
198 )
199 else:
200 self.visual = visual
202 # if we've been passed a visual object
203 if vertex_colors is not None:
204 self.vertex_attributes["color"] = vertex_colors
205 if face_colors is not None:
206 self.face_attributes["color"] = face_colors
208 # normals are accessed through setters/properties and are regenerated
209 # if dimensions are inconsistent, but can be set by the constructor
210 # to avoid a substantial number of cross products
211 if face_normals is not None:
212 self.face_normals = face_normals
214 # (n, 3) float of vertex normals, can be created from face normals
215 if vertex_normals is not None:
216 self.vertex_normals = vertex_normals
218 # embree is a much, much faster raytracer written by Intel
219 # if you have pyembree installed you should use it
220 # although both raytracers were designed to have a common API
221 if ray.has_embree and use_embree:
222 self.ray = ray.ray_pyembree.RayMeshIntersector(self)
223 else:
224 # create a ray-mesh query object for the current mesh
225 # initializing is very inexpensive and object is convenient to have.
226 # On first query expensive bookkeeping is done (creation of r-tree),
227 # and is cached for subsequent queries
228 self.ray = ray.ray_triangle.RayMeshIntersector(self)
230 # a quick way to get permuted versions of the current mesh
231 self.permutate = permutate.Permutator(self)
233 # convenience class for nearest point queries
234 self.nearest = proximity.ProximityQuery(self)
236 # update the mesh metadata with passed metadata
237 self.metadata = {}
238 if isinstance(metadata, dict):
239 self.metadata.update(metadata)
240 elif metadata is not None:
241 raise ValueError(f"metadata should be a dict or None, got {metadata!s}")
243 # use update to copy items
244 if face_attributes is not None:
245 self.face_attributes.update(face_attributes)
246 if vertex_attributes is not None:
247 self.vertex_attributes.update(vertex_attributes)
249 # process will remove NaN and Inf values and merge vertices
250 # if validate, will remove degenerate and duplicate faces
251 if process or validate:
252 self.process(validate=validate, merge_tex=merge_tex, merge_norm=merge_norm)
254 def process(
255 self,
256 validate: bool = False,
257 merge_tex: bool | None = None,
258 merge_norm: bool | None = None,
259 ) -> Self:
260 """
261 Do processing to make a mesh useful.
263 Does this by:
264 1) removing NaN and Inf values
265 2) merging duplicate vertices
266 If validate:
267 3) Remove triangles which have one edge
268 of their 2D oriented bounding box
269 shorter than tol.merge
270 4) remove duplicated triangles
271 5) Attempt to ensure triangles are consistently wound
272 and normals face outwards.
274 Parameters
275 ------------
276 validate : bool
277 Remove degenerate and duplicate faces.
278 merge_tex : bool
279 If True textured meshes with UV coordinates will
280 have vertices merged regardless of UV coordinates
281 merge_norm : bool
282 If True, meshes with vertex normals will have
283 vertices merged ignoring different normals
285 Returns
286 ------------
287 self: trimesh.Trimesh
288 Current mesh
289 """
290 # if there are no vertices or faces exit early
291 if self.is_empty:
292 return self
294 # if we're cleaning remove duplicate and degenerate faces. this
295 # mutates face count so it must run OUTSIDE the cache lock — locking
296 # across a face-count change leaves derived caches (face_adjacency,
297 # edges, ...) stale, which fix_normals would then read and blow up on.
298 if validate:
299 # get a mask with only unique and non-degenerate faces
300 mask = self.unique_faces() & self.nondegenerate_faces()
301 self.update_faces(mask)
302 self.fix_normals()
304 # the remaining ops do not change face/vertex count so we can hold
305 # the cache lock to preserve face_normals/vertex_normals across them
306 with self._cache:
307 self.remove_infinite_values()
308 self.merge_vertices(merge_tex=merge_tex, merge_norm=merge_norm)
309 self._cache.clear(exclude={"face_normals", "vertex_normals"})
311 self.metadata["processed"] = True
312 return self
314 @property
315 def mutable(self) -> bool:
316 """
317 Is the current mesh allowed to be altered in-place?
319 Returns
320 -------------
321 mutable
322 If data is allowed to be set for the mesh.
323 """
324 return self._data.mutable
326 @mutable.setter
327 def mutable(self, value: bool) -> None:
328 """
329 Set the mutability of the current mesh.
331 Parameters
332 ----------
333 value
334 Change whether the current mesh is allowed to be altered in-place.
335 """
336 self._data.mutable = value
338 @property
339 def faces(self) -> TrackedArray:
340 """
341 The faces of the mesh.
343 This is regarded as core information which cannot be
344 regenerated from cache and as such is stored in
345 `self._data` which tracks the array for changes and
346 clears cached values of the mesh altered.
348 Returns
349 ----------
350 faces : (n, 3) int64
351 References for `self.vertices` for triangles.
352 """
353 return self._data["faces"]
355 @faces.setter
356 def faces(self, values: ArrayLike | None) -> None:
357 """
358 Set the vertex indexes that make up triangular faces.
360 Parameters
361 --------------
362 values : (n, 3) int64
363 Indexes of self.vertices
364 """
365 if values is None:
366 # if passed none store an empty array
367 values = np.zeros(shape=(0, 3), dtype=int64)
368 else:
369 values = np.asanyarray(values, dtype=int64)
371 # automatically triangulate quad faces
372 if len(values.shape) == 2 and values.shape[1] != 3:
373 log.info("triangulating faces")
374 values = geometry.triangulate_quads(values)
376 self._data["faces"] = values
378 @cache_decorator
379 def faces_sparse(self) -> coo_matrix:
380 """
381 A sparse matrix representation of the faces.
383 Returns
384 ----------
385 sparse : scipy.sparse.coo_matrix
386 Has properties:
387 dtype : bool
388 shape : (len(self.vertices), len(self.faces))
389 """
390 return geometry.index_sparse(columns=len(self.vertices), indices=self.faces)
392 @property
393 def face_normals(self) -> NDArray[float64]:
394 """
395 Return the unit normal vector for each face.
397 If a face is degenerate and a normal can't be generated
398 a zero magnitude unit vector will be returned for that face.
400 Returns
401 -----------
402 normals : (len(self.faces), 3) float64
403 Normal vectors of each face
404 """
405 # check shape of cached normals
406 cached = self._cache["face_normals"]
407 # get faces from datastore
408 if "faces" in self._data:
409 faces = self._data.data["faces"]
410 else:
411 faces = None
413 # if we have no faces exit early
414 if faces is None or len(faces) == 0:
415 return np.array([], dtype=float64).reshape((0, 3))
417 # if the shape of cached normals equals the shape of faces return
418 if np.shape(cached) == np.shape(faces):
419 return cached
421 # use cached triangle cross products to generate normals
422 # this will always return the correct shape but some values
423 # will be zero or an arbitrary vector if the inputs had
424 # a cross product below machine epsilon
425 normals, valid = triangles.normals(
426 triangles=self.triangles, crosses=self.triangles_cross
427 )
429 # if all triangles are valid shape is correct
430 if valid.all():
431 # put calculated face normals into cache manually
432 self._cache["face_normals"] = normals
433 return normals
435 # make a padded list of normals for correct shape
436 padded = np.zeros((len(self.triangles), 3), dtype=float64)
437 padded[valid] = normals
439 # put calculated face normals into cache manually
440 self._cache["face_normals"] = padded
442 return padded
444 @face_normals.setter
445 def face_normals(self, values: ArrayLike | None) -> None:
446 """
447 Assign values to face normals.
449 Parameters
450 -------------
451 values : (len(self.faces), 3) float
452 Unit face normals. If None will clear existing normals.
453 """
454 # if nothing passed exit
455 if values is None:
456 return
457 # make sure candidate face normals are C-contiguous float
458 values = np.asanyarray(values, order="C", dtype=float64)
459 # face normals need to correspond to faces
460 if len(values) == 0 or values.shape != self.faces.shape:
461 log.debug("face_normals incorrect shape, ignoring!")
462 return
463 # check if any values are larger than tol.merge
464 # don't set the normals if they are all zero
465 ptp = np.ptp(values)
466 if not np.isfinite(ptp):
467 log.debug("face_normals contain NaN, ignoring!")
468 return
469 if ptp < tol.merge:
470 log.debug("face_normals all zero, ignoring!")
471 return
473 # make sure the first few normals match the first few triangles
474 check, valid = triangles.normals(self.vertices.view(np.ndarray)[self.faces[:20]])
475 compare = np.zeros((len(valid), 3))
476 compare[valid] = check
477 if not np.allclose(compare, values[:20]):
478 log.debug("face_normals didn't match triangles, ignoring!")
479 return
481 # otherwise store face normals
482 self._cache["face_normals"] = values
484 @property
485 def vertices(self) -> TrackedArray:
486 """
487 The vertices of the mesh.
489 This is regarded as core information which cannot be
490 generated from cache and as such is stored in self._data
491 which tracks the array for changes and clears cached
492 values of the mesh if this is altered.
494 Returns
495 ----------
496 vertices : (n, 3) float
497 Points in cartesian space referenced by self.faces
498 """
499 # get vertices if already stored
500 return self._data["vertices"]
502 @vertices.setter
503 def vertices(self, values: ArrayLike | None) -> None:
504 """
505 Assign vertex values to the mesh.
507 Parameters
508 --------------
509 values : (n, 3) float
510 Points in space
511 """
512 if values is None:
513 # remove any stored data and store an empty array
514 values = np.zeros(shape=(0, 3), dtype=float64)
515 self._data["vertices"] = np.asanyarray(values, order="C", dtype=float64)
517 @cache_decorator
518 def vertex_normals(self) -> NDArray[float64]:
519 """
520 The vertex normals of the mesh. If the normals were loaded
521 we check to make sure we have the same number of vertex
522 normals and vertices before returning them. If there are
523 no vertex normals defined or a shape mismatch we calculate
524 the vertex normals from the mean normals of the faces the
525 vertex is used in.
527 Returns
528 ----------
529 vertex_normals : (n, 3) float
530 Represents the surface normal at each vertex.
531 Where n == len(self.vertices)
532 """
533 # make sure we have faces_sparse
534 return geometry.weighted_vertex_normals(
535 vertex_count=len(self.vertices),
536 faces=self.faces,
537 face_normals=self.face_normals,
538 face_angles=self.face_angles,
539 )
541 @vertex_normals.setter
542 def vertex_normals(self, values: ArrayLike) -> None:
543 """
544 Assign values to vertex normals.
546 Parameters
547 -------------
548 values : (len(self.vertices), 3) float
549 Unit normal vectors for each vertex
550 """
551 if values is not None:
552 values = np.asanyarray(values, order="C", dtype=float64)
553 if values.shape == self.vertices.shape:
554 # check to see if they assigned all zeros
555 if np.ptp(values) < tol.merge:
556 log.debug("vertex_normals are all zero!")
557 self._cache["vertex_normals"] = values
559 @cache_decorator
560 def vertex_faces(self) -> NDArray[int64]:
561 """
562 A representation of the face indices that correspond to each vertex.
564 Returns
565 ----------
566 vertex_faces : (n,m) int
567 Each row contains the face indices that correspond to the given vertex,
568 padded with -1 up to the max number of faces corresponding to any one vertex
569 Where n == len(self.vertices), m == max number of faces for a single vertex
570 """
571 vertex_faces = geometry.vertex_face_indices(
572 vertex_count=len(self.vertices),
573 faces=self.faces,
574 faces_sparse=self.faces_sparse,
575 )
576 return vertex_faces
578 @cache_decorator
579 def bounds(self) -> NDArray[float64] | None:
580 """
581 The axis aligned bounds of the faces of the mesh.
583 Returns
584 -----------
585 bounds : (2, 3) float or None
586 Bounding box with [min, max] coordinates
587 If mesh is empty will return None
588 """
589 # return bounds including ONLY referenced vertices
590 in_mesh = self.vertices[self.referenced_vertices]
591 # don't crash if we have no vertices referenced
592 if len(in_mesh) == 0:
593 return None
594 # get mesh bounds with min and max
595 return np.array([in_mesh.min(axis=0), in_mesh.max(axis=0)])
597 @cache_decorator
598 def extents(self) -> NDArray[float64] | None:
599 """
600 The length, width, and height of the axis aligned
601 bounding box of the mesh.
603 Returns
604 -----------
605 extents : (3, ) float or None
606 Array containing axis aligned [length, width, height]
607 If mesh is empty returns None
608 """
609 # if mesh is empty return None
610 if self.bounds is None:
611 return None
612 extents = np.ptp(self.bounds, axis=0)
614 return extents
616 @cache_decorator
617 def centroid(self) -> NDArray[float64]:
618 """
619 The point in space which is the average of the triangle
620 centroids weighted by the area of each triangle.
622 This will be valid even for non-watertight meshes,
623 unlike self.center_mass
625 Returns
626 ----------
627 centroid : (3, ) float
628 The average vertex weighted by face area
629 """
631 # use the centroid of each triangle weighted by
632 # the area of the triangle to find the overall centroid
633 try:
634 centroid = np.average(self.triangles_center, weights=self.area_faces, axis=0)
635 except BaseException:
636 # if all triangles are zero-area weights will not work
637 centroid = self.triangles_center.mean(axis=0)
638 return centroid
640 @property
641 def center_mass(self) -> NDArray[float64]:
642 """
643 The point in space which is the center of mass/volume.
645 Returns
646 -----------
647 center_mass : (3, ) float
648 Volumetric center of mass of the mesh.
649 """
650 return self.mass_properties.center_mass
652 @center_mass.setter
653 def center_mass(self, value: ArrayLike) -> None:
654 """
655 Override the point in space which is the center of mass and volume.
657 Parameters
658 -----------
659 center_mass : (3, ) float
660 Volumetric center of mass of the mesh.
661 """
662 value = np.array(value, dtype=float64)
663 if value.shape != (3,):
664 raise ValueError("shape must be (3,) float!")
665 self._data["center_mass"] = value
666 self._cache.delete("mass_properties")
668 @property
669 def density(self) -> float:
670 """
671 The density of the mesh used in inertia calculations.
673 Returns
674 -----------
675 density
676 The density of the primitive.
677 """
678 return float(self.mass_properties.density)
680 @density.setter
681 def density(self, value: Number) -> None:
682 """
683 Set the density of the primitive.
685 Parameters
686 -------------
687 density
688 Specify the density of the primitive to be
689 used in inertia calculations.
690 """
691 self._data["density"] = float(value)
692 self._cache.delete("mass_properties")
694 @property
695 def volume(self) -> float64:
696 """
697 Volume of the current mesh calculated using a surface
698 integral. If the current mesh isn't watertight this is
699 garbage.
701 Returns
702 ---------
703 volume : float
704 Volume of the current mesh
705 """
706 return self.mass_properties.volume
708 @property
709 def mass(self) -> float64:
710 """
711 Mass of the current mesh, based on specified density and
712 volume. If the current mesh isn't watertight this is garbage.
714 Returns
715 ---------
716 mass : float
717 Mass of the current mesh
718 """
719 return self.mass_properties.mass
721 @property
722 def moment_inertia(self) -> NDArray[float64]:
723 """
724 Return the moment of inertia matrix of the current mesh.
725 If mesh isn't watertight this is garbage. The returned
726 moment of inertia is *axis aligned* at the mesh's center
727 of mass `mesh.center_mass`. If you want the moment at any
728 other frame including the origin call:
729 `mesh.moment_inertia_frame`
731 Returns
732 ---------
733 inertia : (3, 3) float
734 Moment of inertia of the current mesh at the center of
735 mass and aligned with the cartesian axis.
736 """
737 return self.mass_properties.inertia
739 def moment_inertia_frame(self, transform: ArrayLike) -> NDArray[float64]:
740 """
741 Get the moment of inertia of this mesh with respect to
742 an arbitrary frame, versus with respect to the center
743 of mass as returned by `mesh.moment_inertia`.
745 For example if `transform` is an identity matrix `np.eye(4)`
746 this will give the moment at the origin.
748 Uses the parallel axis theorum to move the center mass
749 tensor to this arbitrary frame.
751 Parameters
752 ------------
753 transform : (4, 4) float
754 Homogeneous transformation matrix.
756 Returns
757 -------------
758 inertia : (3, 3)
759 Moment of inertia in the requested frame.
760 """
761 # we'll need the inertia tensor and the center of mass
762 props = self.mass_properties
763 # calculated moment of inertia is at the center of mass
764 # so we want to offset our requested translation by that
765 # center of mass
766 offset = np.eye(4)
767 offset[:3, 3] = -props["center_mass"]
769 # apply the parallel axis theorum to get the new inertia
770 return inertia.transform_inertia(
771 inertia_tensor=props["inertia"],
772 transform=np.dot(offset, transform),
773 mass=props["mass"],
774 parallel_axis=True,
775 )
777 @cache_decorator
778 def principal_inertia_components(self) -> NDArray[float64]:
779 """
780 Return the principal components of inertia
782 Ordering corresponds to mesh.principal_inertia_vectors
784 Returns
785 ----------
786 components : (3, ) float
787 Principal components of inertia
788 """
789 # both components and vectors from inertia matrix
790 components, vectors = inertia.principal_axis(self.moment_inertia)
791 # store vectors in cache for later
792 self._cache["principal_inertia_vectors"] = vectors
794 return components
796 @property
797 def principal_inertia_vectors(self) -> NDArray[float64]:
798 """
799 Return the principal axis of inertia as unit vectors.
800 The order corresponds to `mesh.principal_inertia_components`.
802 Returns
803 ----------
804 vectors : (3, 3) float
805 Three vectors pointing along the
806 principal axis of inertia directions
807 """
808 _ = self.principal_inertia_components
809 return self._cache["principal_inertia_vectors"]
811 @cache_decorator
812 def principal_inertia_transform(self) -> NDArray[float64]:
813 """
814 A transform which moves the current mesh so the principal
815 inertia vectors are on the X,Y, and Z axis, and the centroid is
816 at the origin.
818 Returns
819 ----------
820 transform : (4, 4) float
821 Homogeneous transformation matrix
822 """
823 order = np.argsort(self.principal_inertia_components)[1:][::-1]
824 vectors = self.principal_inertia_vectors[order]
825 vectors = np.vstack((vectors, np.cross(*vectors)))
827 transform = np.eye(4)
828 transform[:3, :3] = vectors
829 transform = transformations.transform_around(
830 matrix=transform, point=self.centroid
831 )
832 transform[:3, 3] -= self.centroid
834 return transform
836 @cache_decorator
837 def symmetry(self) -> str | None:
838 """
839 Check whether a mesh has rotational symmetry around
840 an axis (radial) or point (spherical).
842 Returns
843 -----------
844 symmetry : None, 'radial', 'spherical'
845 What kind of symmetry does the mesh have.
846 """
847 symmetry, axis, section = inertia.radial_symmetry(self)
848 self._cache["symmetry_axis"] = axis
849 self._cache["symmetry_section"] = section
850 return symmetry
852 @property
853 def symmetry_axis(self) -> NDArray[float64] | None:
854 """
855 If a mesh has rotational symmetry, return the axis.
857 Returns
858 ------------
859 axis : (3, ) float
860 Axis around which a 2D profile was revolved to create this mesh.
861 """
862 if self.symmetry is None:
863 return None
864 return self._cache["symmetry_axis"]
866 @property
867 def symmetry_section(self) -> NDArray[float64] | None:
868 """
869 If a mesh has rotational symmetry return the two
870 vectors which make up a section coordinate frame.
872 Returns
873 ----------
874 section : (2, 3) float
875 Vectors to take a section along
876 """
877 if self.symmetry is None:
878 return None
879 return self._cache["symmetry_section"]
881 @cache_decorator
882 def triangles(self) -> NDArray[float64]:
883 """
884 Actual triangles of the mesh (points, not indexes)
886 Returns
887 ---------
888 triangles : (n, 3, 3) float
889 Points of triangle vertices
890 """
891 # use of advanced indexing on our tracked arrays will
892 # trigger a change flag which means the hash will have to be
893 # recomputed. We can escape this check by viewing the array.
894 return self.vertices.view(np.ndarray)[self.faces]
896 @cache_decorator
897 def triangles_tree(self) -> Index:
898 """
899 An R-tree containing each face of the mesh.
901 Returns
902 ----------
903 tree : rtree.index
904 Each triangle in self.faces has a rectangular cell
905 """
906 return triangles.bounds_tree(self.triangles)
908 @cache_decorator
909 def triangles_center(self) -> NDArray[float64]:
910 """
911 The center of each triangle (barycentric [1/3, 1/3, 1/3])
913 Returns
914 ---------
915 triangles_center : (len(self.faces), 3) float
916 Center of each triangular face
917 """
918 return self.triangles.mean(axis=1)
920 @cache_decorator
921 def triangles_cross(self) -> NDArray[float64]:
922 """
923 The cross product of two edges of each triangle.
925 Returns
926 ---------
927 crosses : (n, 3) float
928 Cross product of each triangle
929 """
930 crosses = triangles.cross(self.triangles)
931 return crosses
933 @cache_decorator
934 def edges(self) -> NDArray[int64]:
935 """
936 Edges of the mesh (derived from faces).
938 Returns
939 ---------
940 edges : (n, 2) int
941 List of vertex indices making up edges
942 """
943 edges, index = geometry.faces_to_edges(
944 self.faces.view(np.ndarray), return_index=True
945 )
946 self._cache["edges_face"] = index
947 return edges
949 @cache_decorator
950 def edges_face(self) -> NDArray[int64]:
951 """
952 Which face does each edge belong to.
954 Returns
955 ---------
956 edges_face : (n, ) int
957 Index of self.faces
958 """
959 _ = self.edges
960 return self._cache["edges_face"]
962 @cache_decorator
963 def edges_unique(self) -> NDArray[int64]:
964 """
965 The unique edges of the mesh.
967 Returns
968 ----------
969 edges_unique : (n, 2) int
970 Vertex indices for unique edges
971 """
972 unique, inverse = grouping.unique_rows(self.edges_sorted)
973 edges_unique = self.edges_sorted[unique]
974 # edges_unique will be added automatically by the decorator
975 # additional terms generated need to be added to the cache manually
976 self._cache["edges_unique_idx"] = unique
977 self._cache["edges_unique_inverse"] = inverse
978 return edges_unique
980 @cache_decorator
981 def edges_unique_length(self) -> NDArray[float64]:
982 """
983 How long is each unique edge.
985 Returns
986 ----------
987 length : (len(self.edges_unique), ) float
988 Length of each unique edge
989 """
990 vector = np.subtract(*self.vertices[self.edges_unique.T])
991 length = util.row_norm(vector)
992 return length
994 @cache_decorator
995 def edges_unique_inverse(self) -> NDArray[int64]:
996 """
997 Return the inverse required to reproduce
998 self.edges_sorted from self.edges_unique.
1000 Useful for referencing edge properties:
1001 mesh.edges_unique[mesh.edges_unique_inverse] == m.edges_sorted
1003 Returns
1004 ----------
1005 inverse : (len(self.edges), ) int
1006 Indexes of self.edges_unique
1007 """
1008 _ = self.edges_unique
1009 return self._cache["edges_unique_inverse"]
1011 @cache_decorator
1012 def edges_sorted(self) -> NDArray[int64]:
1013 """
1014 Edges sorted along axis 1
1016 Returns
1017 ----------
1018 edges_sorted : (n, 2)
1019 Same as self.edges but sorted along axis 1
1020 """
1021 edges_sorted = np.sort(self.edges, axis=1)
1022 return edges_sorted
1024 @cache_decorator
1025 def edges_sorted_tree(self) -> cKDTree:
1026 """
1027 A KDTree for mapping edges back to edge index.
1029 Returns
1030 ------------
1031 tree : scipy.spatial.cKDTree
1032 Tree when queried with edges will return
1033 their index in mesh.edges_sorted
1034 """
1035 return cKDTree(self.edges_sorted)
1037 @cache_decorator
1038 def edges_sparse(self) -> coo_matrix:
1039 """
1040 Edges in sparse bool COO graph format where connected
1041 vertices are True.
1043 Returns
1044 ----------
1045 sparse: (len(self.vertices), len(self.vertices)) bool
1046 Sparse graph in COO format
1047 """
1048 sparse = graph.edges_to_coo(self.edges, count=len(self.vertices))
1049 return sparse
1051 @cache_decorator
1052 def body_count(self) -> int:
1053 """
1054 How many connected groups of vertices exist in this mesh.
1055 Note that this number may differ from result in mesh.split,
1056 which is calculated from FACE rather than vertex adjacency.
1058 Returns
1059 -----------
1060 count : int
1061 Number of connected vertex groups
1062 """
1063 # labels are (len(vertices), int) OB
1064 count, labels = graph.csgraph.connected_components(
1065 self.edges_sparse, directed=False, return_labels=True
1066 )
1067 self._cache["vertices_component_label"] = labels
1068 return count
1070 @cache_decorator
1071 def faces_unique_edges(self) -> NDArray[int64]:
1072 """
1073 For each face return which indexes in mesh.unique_edges constructs
1074 that face.
1076 Returns
1077 ---------
1078 faces_unique_edges : (len(self.faces), 3) int
1079 Indexes of self.edges_unique that
1080 construct self.faces
1082 Examples
1083 ---------
1084 In [0]: mesh.faces[:2]
1085 Out[0]:
1086 TrackedArray([[ 1, 6946, 24224],
1087 [ 6946, 1727, 24225]])
1089 In [1]: mesh.edges_unique[mesh.faces_unique_edges[:2]]
1090 Out[1]:
1091 array([[[ 1, 6946],
1092 [ 6946, 24224],
1093 [ 1, 24224]],
1094 [[ 1727, 6946],
1095 [ 1727, 24225],
1096 [ 6946, 24225]]])
1097 """
1098 # make sure we have populated unique edges
1099 _ = self.edges_unique
1100 # we are relying on the fact that edges are stacked in triplets
1101 result = self._cache["edges_unique_inverse"].reshape((-1, 3))
1102 return result
1104 @cache_decorator
1105 def euler_number(self) -> int:
1106 """
1107 Return the Euler characteristic (a topological invariant) for the mesh
1108 In order to guarantee correctness, this should be called after
1109 remove_unreferenced_vertices
1111 Returns
1112 ----------
1113 euler_number : int
1114 Topological invariant
1115 """
1116 return int(
1117 self.referenced_vertices.sum() - len(self.edges_unique) + len(self.faces)
1118 )
1120 @cache_decorator
1121 def referenced_vertices(self) -> NDArray[np.bool_]:
1122 """
1123 Which vertices in the current mesh are referenced by a face.
1125 Returns
1126 -------------
1127 referenced : (len(self.vertices), ) bool
1128 Which vertices are referenced by a face
1129 """
1130 referenced = np.zeros(len(self.vertices), dtype=bool)
1131 referenced[self.faces] = True
1132 return referenced
1134 def convert_units(self, desired: str, guess: bool = False) -> Self:
1135 """
1136 Convert the units of the mesh into a specified unit.
1138 Parameters
1139 ------------
1140 desired : string
1141 Units to convert to (eg 'inches')
1142 guess : boolean
1143 If self.units are not defined should we
1144 guess the current units of the document and then convert?
1146 Returns
1147 ------------
1148 self: trimesh.Trimesh
1149 Current mesh
1150 """
1151 units._convert_units(self, desired, guess)
1152 return self
1154 def merge_vertices(
1155 self,
1156 merge_tex: bool | None = None,
1157 merge_norm: bool | None = None,
1158 digits_vertex: Integer | None = None,
1159 digits_norm: Integer | None = None,
1160 digits_uv: Integer | None = None,
1161 ) -> None:
1162 """
1163 Removes duplicate vertices grouped by position and
1164 optionally texture coordinate and normal.
1166 Parameters
1167 -------------
1168 merge_tex : bool
1169 If True textured meshes with UV coordinates will
1170 have vertices merged regardless of UV coordinates
1171 merge_norm : bool
1172 If True, meshes with vertex normals will have
1173 vertices merged ignoring different normals
1174 digits_vertex : None or int
1175 Number of digits to consider for vertex position
1176 digits_norm : int
1177 Number of digits to consider for unit normals
1178 digits_uv : int
1179 Number of digits to consider for UV coordinates
1180 """
1181 grouping.merge_vertices(
1182 mesh=self,
1183 merge_tex=merge_tex,
1184 merge_norm=merge_norm,
1185 digits_vertex=digits_vertex,
1186 digits_norm=digits_norm,
1187 digits_uv=digits_uv,
1188 )
1190 def update_vertices(
1191 self,
1192 mask: ArrayLike,
1193 inverse: ArrayLike | None = None,
1194 ) -> None:
1195 """
1196 Update vertices with a mask.
1198 Parameters
1199 ------------
1200 mask : (len(self.vertices)) bool
1201 Array of which vertices to keep
1202 inverse : (len(self.vertices)) int
1203 Array to reconstruct vertex references
1204 such as output by np.unique
1205 """
1206 # if the mesh is already empty we can't remove anything
1207 if self.is_empty:
1208 return
1210 # make sure mask is a numpy array
1211 mask = np.asanyarray(mask)
1213 if (mask.dtype.name == "bool" and mask.all()) or len(mask) == 0 or self.is_empty:
1214 # mask doesn't remove any vertices so exit early
1215 return
1217 # create the inverse mask if not passed
1218 if inverse is None:
1219 inverse = np.zeros(len(self.vertices), dtype=int64)
1220 if mask.dtype.kind == "b":
1221 inverse[mask] = np.arange(mask.sum())
1222 elif mask.dtype.kind == "i":
1223 inverse[mask] = np.arange(len(mask))
1224 else:
1225 inverse = None
1227 # re-index faces from inverse
1228 if inverse is not None and util.is_shape(self.faces, (-1, 3)):
1229 self.faces = inverse[self.faces.reshape(-1)].reshape((-1, 3))
1231 # update the visual object with our mask
1232 self.visual.update_vertices(mask)
1233 # get the normals from cache before dumping
1234 cached_normals = self._cache["vertex_normals"]
1236 # apply to face_attributes
1237 count = len(self.vertices)
1238 for key, value in self.vertex_attributes.items():
1239 try:
1240 # covers un-len'd objects as well
1241 if len(value) != count:
1242 raise TypeError()
1243 except TypeError:
1244 continue
1245 # apply the mask to the attribute
1246 self.vertex_attributes[key] = value[mask]
1248 # actually apply the mask
1249 self.vertices = self.vertices[mask]
1251 # if we had passed vertex normals try to save them
1252 if util.is_shape(cached_normals, (-1, 3)):
1253 try:
1254 self.vertex_normals = cached_normals[mask]
1255 except BaseException:
1256 pass
1258 def update_faces(self, mask: ArrayLike) -> None:
1259 """
1260 In many cases, we will want to remove specific faces.
1261 However, there is additional bookkeeping to do this cleanly.
1262 This function updates the set of faces with a validity mask,
1263 as well as keeping track of normals and colors.
1265 Parameters
1266 ------------
1267 mask : (m) int or (len(self.faces)) bool
1268 Mask to remove faces
1269 """
1270 # if the mesh is already empty we can't remove anything
1271 if self.is_empty:
1272 return
1274 mask = np.asanyarray(mask)
1275 if mask.dtype.name == "bool" and mask.all():
1276 # mask removes no faces so exit early
1277 return
1279 # try to save face normals before dumping cache
1280 cached_normals = self._cache["face_normals"]
1282 faces = self._data["faces"]
1283 # if Trimesh has been subclassed and faces have been moved
1284 # from data to cache, get faces from cache.
1285 if not util.is_shape(faces, (-1, 3)):
1286 faces = self._cache["faces"]
1288 # apply to face_attributes
1289 count = len(self.faces)
1290 for key, value in self.face_attributes.items():
1291 try:
1292 # covers un-len'd objects as well
1293 if len(value) != count:
1294 raise TypeError()
1295 except TypeError:
1296 continue
1297 # apply the mask to the attribute
1298 self.face_attributes[key] = value[mask]
1300 # actually apply the mask
1301 self.faces = faces[mask]
1303 # apply to face colors
1304 self.visual.update_faces(mask)
1306 # if our normals were the correct shape apply them
1307 if util.is_shape(cached_normals, (-1, 3)):
1308 self.face_normals = cached_normals[mask]
1310 def extend_faces(self, new_faces: ArrayLike):
1311 """
1312 Extend `mesh.faces` in-place with new triangles.
1314 This does substantial bookkeeping: padding face colors
1315 and face attributes with default values, and preserving cached
1316 face normals to avoid recomputing every normal.
1318 Parameters
1319 ------------
1320 new_faces : (n, 3) integer
1321 The new faces as indexes of `self.vertices`
1322 """
1323 new_faces = np.asanyarray(new_faces, dtype=np.int64)
1324 if len(new_faces.shape) != 2 or new_faces.shape[1] != 3:
1325 raise ValueError(f"Faces must be triangular, not `{new_faces.shape}`!")
1327 if len(new_faces) == 0:
1328 return
1330 # make sure the cache is up-to-date
1331 self._cache.verify()
1333 # if we manage to extend colors and normals
1334 extend_normals, extend_colors = None, None
1336 # always filter degenerate triangles
1337 new_normals, valid = triangles.normals(self.vertices[new_faces])
1338 new_faces = new_faces[valid]
1339 if len(new_faces) == 0:
1340 return
1342 # save cached normals if available to avoid a full recompute
1343 if "face_normals" in self._cache.cache:
1344 cached_normals = self._cache.cache["face_normals"]
1345 if len(cached_normals) > 0:
1346 extend_normals = util.vstack_empty((cached_normals, new_normals))
1348 if self.visual.defined and self.visual.kind == "face":
1349 extend_colors = util.vstack_empty(
1350 (
1351 self.visual.face_colors,
1352 np.tile(visual.DEFAULT_COLOR, (len(new_faces), 1)),
1353 )
1354 )
1356 ##########
1357 # DO ALL MUTATION AT THE END HERE
1358 # apply the new faces
1359 original_length = len(self._data["faces"])
1360 self.faces = util.vstack_empty((self._data["faces"], new_faces))
1361 # dump the cache to set the new hash to the stacked faces
1362 self._cache.verify()
1363 # save us a normals recompute if we can
1364 if extend_normals is not None:
1365 self._cache["face_normals"] = extend_normals
1366 if extend_colors is not None:
1367 self.visual.face_colors = extend_colors
1369 # collect new, padded face attributes
1370 new_attribs = {}
1371 for name, attrib in self.face_attributes.items():
1372 shape = np.shape(attrib)
1373 if len(shape) == 0 or shape[0] != original_length:
1374 continue
1375 # pad integers with -1 and everything else with zeros
1376 fill = -1 if attrib.dtype.kind == "i" else 0
1377 pad_shape = (len(new_faces),) + shape[1:]
1378 pad = np.full(pad_shape, fill, dtype=attrib.dtype)
1379 new_attribs[name] = np.concatenate((attrib, pad))
1380 # update outside the loop with new values
1381 self.face_attributes.update(new_attribs)
1383 def remove_infinite_values(self) -> None:
1384 """
1385 Ensure that every vertex and face consists of finite numbers.
1386 This will remove vertices or faces containing np.nan and np.inf
1388 Alters `self.faces` and `self.vertices`
1389 """
1390 if util.is_shape(self.faces, (-1, 3)):
1391 # (len(self.faces), ) bool, mask for faces
1392 face_mask = np.isfinite(self.faces).all(axis=1)
1393 self.update_faces(face_mask)
1395 if util.is_shape(self.vertices, (-1, 3)):
1396 # (len(self.vertices), ) bool, mask for vertices
1397 vertex_mask = np.isfinite(self.vertices).all(axis=1)
1398 self.update_vertices(vertex_mask)
1400 def unique_faces(self) -> NDArray[np.bool_]:
1401 """
1402 On the current mesh find which faces are unique.
1404 Returns
1405 --------
1406 unique : (len(faces),) bool
1407 A mask where the first occurrence of a unique face is true.
1408 """
1409 mask = np.zeros(len(self.faces), dtype=bool)
1410 mask[grouping.unique_rows(np.sort(self.faces, axis=1))[0]] = True
1411 return mask
1413 def rezero(self) -> None:
1414 """
1415 Translate the mesh so that all vertex vertices are positive
1416 and the lower bound of `self.bounds` will be exactly zero.
1418 Alters `self.vertices`.
1419 """
1420 self.apply_translation(self.bounds[0] * -1.0)
1422 def split(self, **kwargs) -> list["Trimesh"]:
1423 """
1424 Split a mesh into multiple meshes from face
1425 connectivity.
1427 If only_watertight is true it will only return
1428 watertight meshes and will attempt to repair
1429 single triangle or quad holes.
1431 Parameters
1432 ----------
1433 mesh : trimesh.Trimesh
1434 The source multibody mesh to split
1435 only_watertight
1436 Only return watertight components and discard
1437 any connected component that isn't fully watertight.
1438 repair
1439 If set try to fill small holes in a mesh, before the
1440 discard step in `only_watertight.
1441 adjacency : (n, 2) int
1442 If passed will be used instead of `mesh.face_adjacency`
1443 engine
1444 Which graph engine to use for the connected components.
1445 kwargs
1446 Will be passed to `mesh.submesh`
1448 Returns
1449 ----------
1450 meshes : (m,) trimesh.Trimesh
1451 Results of splitting based on parameters.
1452 """
1453 return graph.split(self, **kwargs)
1455 @cache_decorator
1456 def face_adjacency(self) -> NDArray[int64]:
1457 """
1458 Find faces that share an edge i.e. 'adjacent' faces.
1460 Returns
1461 ----------
1462 adjacency : (n, 2) int
1463 Pairs of faces which share an edge
1465 Examples
1466 ---------
1468 In [1]: mesh = trimesh.load('models/featuretype.STL')
1470 In [2]: mesh.face_adjacency
1471 Out[2]:
1472 array([[ 0, 1],
1473 [ 2, 3],
1474 [ 0, 3],
1475 ...,
1476 [1112, 949],
1477 [3467, 3475],
1478 [1113, 3475]])
1480 In [3]: mesh.faces[mesh.face_adjacency[0]]
1481 Out[3]:
1482 TrackedArray([[ 1, 0, 408],
1483 [1239, 0, 1]], dtype=int64)
1485 In [4]: import networkx as nx
1487 In [5]: graph = nx.from_edgelist(mesh.face_adjacency)
1489 In [6]: groups = nx.connected_components(graph)
1490 """
1491 adjacency, edges = graph.face_adjacency(mesh=self, return_edges=True)
1492 self._cache["face_adjacency_edges"] = edges
1493 return adjacency
1495 @cache_decorator
1496 def face_neighborhood(self) -> NDArray[int64]:
1497 """
1498 Find faces that share a vertex i.e. 'neighbors' faces.
1500 Returns
1501 ----------
1502 neighborhood : (n, 2) int
1503 Pairs of faces which share a vertex
1504 """
1505 return graph.face_neighborhood(self)
1507 @cache_decorator
1508 def face_adjacency_edges(self) -> NDArray[int64]:
1509 """
1510 Returns the edges that are shared by the adjacent faces.
1512 Returns
1513 --------
1514 edges : (n, 2) int
1515 Vertex indices which correspond to face_adjacency
1516 """
1517 # this value is calculated as a byproduct of the face adjacency
1518 _ = self.face_adjacency
1519 return self._cache["face_adjacency_edges"]
1521 @cache_decorator
1522 def face_adjacency_edges_tree(self) -> cKDTree:
1523 """
1524 A KDTree for mapping edges back face adjacency index.
1526 Returns
1527 ------------
1528 tree : scipy.spatial.cKDTree
1529 Tree when queried with SORTED edges will return
1530 their index in mesh.face_adjacency
1531 """
1532 return cKDTree(self.face_adjacency_edges)
1534 @cache_decorator
1535 def face_adjacency_angles(self) -> NDArray[float64]:
1536 """
1537 Return the unsigned angle between adjacent faces
1538 in radians.
1540 Note that if you want a signed angle you can easily
1541 use the `face_adjacency_convex` attribute to get a
1542 signed angle with advanced indexing:
1544 ```
1545 # get a sign per face_adacency pair from the "is it convex" boolean
1546 signs = np.array([-1.0, 1.0])[mesh.face_adjacency_convex.astype(np.int64)]
1548 # apply the signs to the angles
1549 angles = mesh.face_adjacency_angles * signs
1550 ```
1552 Returns
1553 --------
1554 adjacency_angle : (len(self.face_adjacency), ) float
1555 Unsigned angle between adjacent faces
1556 corresponding with `self.face_adjacency`
1557 """
1558 # get pairs of unit vectors for adjacent faces
1559 pairs = self.face_normals[self.face_adjacency]
1560 # find the angle between the pairs of vectors
1561 angles = geometry.vector_angle(pairs)
1562 return angles
1564 @cache_decorator
1565 def face_adjacency_projections(self) -> NDArray[float64]:
1566 """
1567 The projection of the non-shared vertex of a triangle onto
1568 its adjacent face
1570 Returns
1571 ----------
1572 projections : (len(self.face_adjacency), ) float
1573 Dot product of vertex
1574 onto plane of adjacent triangle.
1575 """
1576 projections = convex.adjacency_projections(self)
1577 return projections
1579 @cache_decorator
1580 def face_adjacency_convex(self) -> NDArray[np.bool_]:
1581 """
1582 Return faces which are adjacent and locally convex.
1584 What this means is that given faces A and B, the one vertex
1585 in B that is not shared with A, projected onto the plane of A
1586 has a projection that is zero or negative.
1588 Returns
1589 ----------
1590 are_convex : (len(self.face_adjacency), ) bool
1591 Face pairs that are locally convex
1592 """
1593 return self.face_adjacency_projections < tol.merge
1595 @cache_decorator
1596 def face_adjacency_unshared(self) -> NDArray[int64]:
1597 """
1598 Return the vertex index of the two vertices not in the shared
1599 edge between two adjacent faces
1601 Returns
1602 -----------
1603 vid_unshared : (len(mesh.face_adjacency), 2) int
1604 Indexes of mesh.vertices
1605 """
1606 return graph.face_adjacency_unshared(self)
1608 @cache_decorator
1609 def face_adjacency_radius(self) -> NDArray[float64]:
1610 """
1611 The approximate radius of a cylinder that fits inside adjacent faces.
1613 Returns
1614 ------------
1615 radii : (len(self.face_adjacency), ) float
1616 Approximate radius formed by triangle pair
1617 """
1618 radii, self._cache["face_adjacency_span"] = graph.face_adjacency_radius(mesh=self)
1619 return radii
1621 @cache_decorator
1622 def face_adjacency_span(self) -> NDArray[float64]:
1623 """
1624 The approximate perpendicular projection of the non-shared
1625 vertices in a pair of adjacent faces onto the shared edge of
1626 the two faces.
1628 Returns
1629 ------------
1630 span : (len(self.face_adjacency), ) float
1631 Approximate span between the non-shared vertices
1632 """
1633 _ = self.face_adjacency_radius
1634 return self._cache["face_adjacency_span"]
1636 @cache_decorator
1637 def integral_mean_curvature(self) -> float64:
1638 """
1639 The integral mean curvature, or the surface integral of the mean curvature.
1641 Returns
1642 ---------
1643 area : float
1644 Integral mean curvature of mesh
1645 """
1646 edges_length = np.linalg.norm(
1647 np.subtract(*self.vertices[self.face_adjacency_edges.T]), axis=1
1648 )
1649 # assign signs based on convex adjacency of face pairs
1650 signs = np.array([-1.0, 1.0])[self.face_adjacency_convex.astype(np.int64)]
1651 # adjust face adjacency angles with signs to reflect orientation
1652 angles = self.face_adjacency_angles * signs
1653 return (angles * edges_length).sum() * 0.5
1655 @cache_decorator
1656 def vertex_adjacency_graph(self) -> Graph:
1657 """
1658 Returns a networkx graph representing the vertices and their connections
1659 in the mesh.
1661 Returns
1662 ---------
1663 graph: networkx.Graph
1664 Graph representing vertices and edges between
1665 them where vertices are nodes and edges are edges
1667 Examples
1668 ----------
1669 This is useful for getting nearby vertices for a given vertex,
1670 potentially for some simple smoothing techniques.
1672 mesh = trimesh.primitives.Box()
1673 graph = mesh.vertex_adjacency_graph
1674 graph.neighbors(0)
1675 > [1, 2, 3, 4]
1676 """
1678 return graph.vertex_adjacency_graph(mesh=self)
1680 @cache_decorator
1681 def vertex_neighbors(self) -> list[list[int64]]:
1682 """
1683 The vertex neighbors of each vertex of the mesh, determined from
1684 the cached vertex_adjacency_graph, if already existent.
1686 Returns
1687 ----------
1688 vertex_neighbors : (len(self.vertices), ) int
1689 Represents immediate neighbors of each vertex along
1690 the edge of a triangle
1692 Examples
1693 ----------
1694 This is useful for getting nearby vertices for a given vertex,
1695 potentially for some simple smoothing techniques.
1697 >>> mesh = trimesh.primitives.Box()
1698 >>> mesh.vertex_neighbors[0]
1699 [1, 2, 3, 4]
1700 """
1701 return graph.neighbors(edges=self.edges_unique, max_index=len(self.vertices))
1703 @cache_decorator
1704 def is_winding_consistent(self) -> bool:
1705 """
1706 Does the mesh have consistent winding or not.
1707 A mesh with consistent winding has each shared edge
1708 going in an opposite direction from the other in the pair.
1710 Returns
1711 --------
1712 consistent : bool
1713 Is winding is consistent or not
1714 """
1715 if self.is_empty:
1716 return False
1717 # consistent winding check is populated into the cache by is_watertight
1718 _ = self.is_watertight
1719 return self._cache["is_winding_consistent"]
1721 @cache_decorator
1722 def is_watertight(self) -> bool:
1723 """
1724 Check if a mesh is watertight by making sure every edge is
1725 included in two faces.
1727 Returns
1728 ----------
1729 is_watertight : bool
1730 Is mesh watertight or not
1731 """
1732 if self.is_empty:
1733 return False
1734 watertight, winding = graph.is_watertight(
1735 edges=self.edges, edges_sorted=self.edges_sorted
1736 )
1737 self._cache["is_winding_consistent"] = winding
1738 return watertight
1740 @cache_decorator
1741 def is_volume(self) -> bool:
1742 """
1743 Check if a mesh has all the properties required to represent
1744 a valid volume, rather than just a surface.
1746 These properties include being watertight, having consistent
1747 winding and outward facing normals.
1749 Returns
1750 ---------
1751 valid
1752 Does the mesh represent a volume
1753 """
1754 return bool(
1755 self.is_watertight
1756 and self.is_winding_consistent
1757 and np.isfinite(self.center_mass).all()
1758 and self.volume > 0.0
1759 )
1761 @property
1762 def is_empty(self) -> bool:
1763 """
1764 Does the current mesh have data defined.
1766 Returns
1767 --------
1768 empty : bool
1769 If True, no data is set on the current mesh
1770 """
1771 return self._data.is_empty()
1773 @cache_decorator
1774 def is_convex(self) -> bool:
1775 """
1776 Check if a mesh is convex or not.
1778 Returns
1779 ----------
1780 is_convex: bool
1781 Is mesh convex or not
1782 """
1783 if self.is_empty:
1784 return False
1786 is_convex = bool(convex.is_convex(self))
1787 return is_convex
1789 @cache_decorator
1790 def kdtree(self) -> cKDTree:
1791 """
1792 Return a scipy.spatial.cKDTree of the vertices of the mesh.
1793 Not cached as this lead to observed memory issues and segfaults.
1795 Returns
1796 ---------
1797 tree : scipy.spatial.cKDTree
1798 Contains mesh.vertices
1799 """
1800 return cKDTree(self.vertices.view(np.ndarray))
1802 def nondegenerate_faces(self, height: Floating = tol.merge) -> NDArray[np.bool_]:
1803 """
1804 Identify degenerate faces (faces without 3 unique vertex indices)
1805 in the current mesh.
1807 Usage example for removing them:
1808 `mesh.update_faces(mesh.nondegenerate_faces())`
1810 If a height is specified, it will identify any face with a 2D oriented
1811 bounding box with one edge shorter than that height.
1813 If not specified, it will identify any face with a zero normal.
1815 Parameters
1816 ------------
1817 height : float
1818 If specified identifies faces with an oriented bounding
1819 box shorter than this on one side.
1821 Returns
1822 -------------
1823 nondegenerate : (len(self.faces), ) bool
1824 Mask that can be used to remove faces
1825 """
1826 return triangles.nondegenerate(
1827 self.triangles, areas=self.area_faces, height=height
1828 )
1830 @cache_decorator
1831 def facets(self) -> list[NDArray[int64]]:
1832 """
1833 Return a list of face indices for coplanar adjacent faces.
1835 Returns
1836 ---------
1837 facets : (n, ) sequence of (m, ) int
1838 Groups of indexes of self.faces
1839 """
1840 facets = graph.facets(self)
1841 return facets
1843 @cache_decorator
1844 def facets_area(self) -> NDArray[float64]:
1845 """
1846 Return an array containing the area of each facet.
1848 Returns
1849 ---------
1850 area : (len(self.facets), ) float
1851 Total area of each facet (group of faces)
1852 """
1853 # avoid thrashing the cache inside a loop
1854 area_faces = self.area_faces
1855 # sum the area of each group of faces represented by facets
1856 # use native python sum in tight loop as opposed to array.sum()
1857 # as in this case the lower function call overhead of
1858 # native sum provides roughly a 50% speedup
1859 areas = np.array([sum(area_faces[i]) for i in self.facets], dtype=float64)
1860 return areas
1862 @cache_decorator
1863 def facets_normal(self) -> NDArray[float64]:
1864 """
1865 Return the normal of each facet
1867 Returns
1868 ---------
1869 normals: (len(self.facets), 3) float
1870 A unit normal vector for each facet
1871 """
1872 if len(self.facets) == 0:
1873 return np.array([])
1875 area_faces = self.area_faces
1877 # the face index of the largest face in each facet
1878 index = np.array([i[area_faces[i].argmax()] for i in self.facets])
1879 # (n, 3) float, unit normal vectors of facet plane
1880 normals = self.face_normals[index]
1881 # (n, 3) float, points on facet plane
1882 origins = self.vertices[self.faces[:, 0][index]]
1883 # save origins in cache
1884 self._cache["facets_origin"] = origins
1886 return normals
1888 @cache_decorator
1889 def facets_origin(self) -> NDArray[float64]:
1890 """
1891 Return a point on the facet plane.
1893 Returns
1894 ------------
1895 origins : (len(self.facets), 3) float
1896 A point on each facet plane
1897 """
1898 _ = self.facets_normal
1899 return self._cache["facets_origin"]
1901 @cache_decorator
1902 def facets_boundary(self) -> list[NDArray[int64]]:
1903 """
1904 Return the edges which represent the boundary of each facet
1906 Returns
1907 ---------
1908 edges_boundary : sequence of (n, 2) int
1909 Indices of self.vertices
1910 """
1911 # make each row correspond to a single face
1912 edges = self.edges_sorted.reshape((-1, 6))
1913 # get the edges for each facet
1914 edges_facet = [edges[i].reshape((-1, 2)) for i in self.facets]
1915 edges_boundary = [i[grouping.group_rows(i, require_count=1)] for i in edges_facet]
1916 return edges_boundary
1918 @cache_decorator
1919 def facets_on_hull(self) -> NDArray[np.bool_]:
1920 """
1921 Find which facets of the mesh are on the convex hull.
1923 Returns
1924 ---------
1925 on_hull : (len(mesh.facets), ) bool
1926 is A facet on the meshes convex hull or not
1927 """
1928 # if no facets exit early
1929 if len(self.facets) == 0:
1930 return np.array([], dtype=bool)
1932 # facets plane, origin and normal
1933 normals = self.facets_normal
1934 origins = self.facets_origin
1936 # (n, 3) convex hull vertices
1937 convex = self.convex_hull.vertices.view(np.ndarray).copy()
1939 # boolean mask for which facets are on convex hull
1940 on_hull = np.zeros(len(self.facets), dtype=bool)
1942 for i, normal, origin in zip(range(len(normals)), normals, origins):
1943 # a facet plane is on the convex hull if every vertex
1944 # of the convex hull is behind that plane
1945 # which we are checking with dot products
1946 dot = np.dot(normal, (convex - origin).T)
1947 on_hull[i] = (dot < tol.merge).all()
1949 return on_hull
1951 def fix_normals(self, multibody: bool | None = None) -> Self:
1952 """
1953 Find and fix problems with self.face_normals and self.faces
1954 winding direction.
1956 For face normals ensure that vectors are consistently pointed
1957 outwards, and that self.faces is wound in the correct direction
1958 for all connected components.
1960 Parameters
1961 -------------
1962 multibody : None or bool
1963 Fix normals across multiple bodies or if unspecified
1964 check the current `Trimesh.body_count`.
1965 """
1966 if multibody is None:
1967 multibody = self.body_count > 1
1968 repair.fix_normals(self, multibody=multibody)
1969 return self
1971 def fill_holes(self) -> bool:
1972 """
1973 Fill single triangle and single quad holes in the current mesh.
1975 Returns
1976 ----------
1977 watertight : bool
1978 Is the mesh watertight after the function completes
1979 """
1980 return repair.fill_holes(self)
1982 def register(
1983 self, other: Geometry3D | NDArray, **kwargs
1984 ) -> tuple[NDArray[float64], float64]:
1985 """
1986 Align a mesh with another mesh or a PointCloud using
1987 the principal axes of inertia as a starting point which
1988 is refined by iterative closest point.
1990 Parameters
1991 ------------
1992 other : trimesh.Trimesh or (n, 3) float
1993 Mesh or points in space
1994 samples : int
1995 Number of samples from mesh surface to align
1996 icp_first : int
1997 How many ICP iterations for the 9 possible
1998 combinations of
1999 icp_final : int
2000 How many ICP itertations for the closest
2001 candidate from the wider search
2003 Returns
2004 -----------
2005 mesh_to_other : (4, 4) float
2006 Transform to align mesh to the other object
2007 cost : float
2008 Average square distance per point
2009 """
2010 mesh_to_other, cost = registration.mesh_other(mesh=self, other=other, **kwargs)
2011 return mesh_to_other, cost
2013 def compute_stable_poses(
2014 self,
2015 center_mass: NDArray[float64] | None = None,
2016 sigma: Floating = 0.0,
2017 n_samples: Integer = 1,
2018 threshold: Floating = 0.0,
2019 ) -> tuple[NDArray[float64], NDArray[float64]]:
2020 """
2021 Computes stable orientations of a mesh and their quasi-static probabilities.
2023 This method samples the location of the center of mass from a multivariate
2024 gaussian (mean at com, cov equal to identity times sigma) over n_samples.
2025 For each sample, it computes the stable resting poses of the mesh on a
2026 a planar workspace and evaluates the probabilities of landing in
2027 each pose if the object is dropped onto the table randomly.
2029 This method returns the 4x4 homogeneous transform matrices that place
2030 the shape against the planar surface with the z-axis pointing upwards
2031 and a list of the probabilities for each pose.
2032 The transforms and probabilities that are returned are sorted, with the
2033 most probable pose first.
2035 Parameters
2036 ------------
2037 center_mass : (3, ) float
2038 The object center of mass (if None, this method
2039 assumes uniform density and watertightness and
2040 computes a center of mass explicitly)
2041 sigma : float
2042 The covariance for the multivariate gaussian used
2043 to sample center of mass locations
2044 n_samples : int
2045 The number of samples of the center of mass location
2046 threshold : float
2047 The probability value at which to threshold
2048 returned stable poses
2050 Returns
2051 -------
2052 transforms : (n, 4, 4) float
2053 The homogeneous matrices that transform the
2054 object to rest in a stable pose, with the
2055 new z-axis pointing upwards from the table
2056 and the object just touching the table.
2058 probs : (n, ) float
2059 A probability ranging from 0.0 to 1.0 for each pose
2060 """
2061 return poses.compute_stable_poses(
2062 mesh=self,
2063 center_mass=center_mass,
2064 sigma=sigma,
2065 n_samples=n_samples,
2066 threshold=threshold,
2067 )
2069 def subdivide(
2070 self, face_index: ArrayLike | None = None, iterations: Integer | None = None
2071 ) -> "Trimesh":
2072 """
2073 Subdivide a mesh with each subdivided face replaced
2074 with four smaller faces. Will return a copy of current
2075 mesh with subdivided faces.
2077 Parameters
2078 ------------
2079 face_index : (m, ) int or None
2080 If None all faces of mesh will be subdivided
2081 If (m, ) int array of indices: only specified faces will be
2082 subdivided. Note that in this case the mesh will generally
2083 no longer be manifold, as the additional vertex on the midpoint
2084 will not be used by the adjacent faces to the faces specified,
2085 and an additional postprocessing step will be required to
2086 make resulting mesh watertight
2087 iterations : int
2088 If passed will run subdivisions multiple times recursively.
2089 NOT COMPATIBLE with `face_index` and will raise a `ValueError`
2090 if both arguments are passed.
2092 Returns
2093 ------------
2094 mesh: trimesh.Trimesh
2095 The copy of current mesh with subdivided faces.
2096 """
2097 if iterations is not None:
2098 # check that our arguments are executable
2099 if face_index is not None:
2100 raise ValueError("Unable to subdivide a subset with multiple iterations!")
2101 # decrement the next iteration
2102 next_iteration = iterations - 1
2103 # if we've reached zero exit
2104 if next_iteration <= 0:
2105 next_iteration = None
2107 visual = None
2108 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == (
2109 len(self.vertices),
2110 2,
2111 ):
2112 # uv coords divided along with vertices
2113 vertices, faces, attr = remesh.subdivide(
2114 vertices=np.hstack((self.vertices, self.visual.uv)),
2115 faces=self.faces,
2116 face_index=face_index,
2117 vertex_attributes=self.vertex_attributes,
2118 )
2120 # get a copy of the current visuals
2121 visual = self.visual.copy()
2123 # separate uv coords and vertices
2124 vertices, visual.uv = vertices[:, :3], vertices[:, 3:]
2126 else:
2127 # perform the subdivision with vertex attributes
2128 vertices, faces, attr = remesh.subdivide(
2129 vertices=self.vertices,
2130 faces=self.faces,
2131 face_index=face_index,
2132 vertex_attributes=self.vertex_attributes,
2133 )
2135 # create a new mesh
2136 result = Trimesh(
2137 vertices=vertices,
2138 faces=faces,
2139 visual=visual,
2140 vertex_attributes=attr,
2141 process=False,
2142 )
2144 if iterations is not None:
2145 return result.subdivide(iterations=next_iteration)
2147 return result
2149 def subdivide_to_size(
2150 self, max_edge: Number, max_iter: Integer = 10, return_index: bool = False
2151 ) -> "Trimesh | tuple[Trimesh, NDArray[int64]]":
2152 """
2153 Subdivide a mesh until every edge is shorter than a
2154 specified length.
2156 Will return a triangle soup, not a nicely structured mesh.
2158 Parameters
2159 ------------
2160 max_edge
2161 Maximum length of any edge in the result
2162 max_iter : int
2163 The maximum number of times to run subdivision
2164 return_index : bool
2165 If True, return index of original face for new faces
2167 Returns
2168 ------------
2169 mesh: trimesh.Trimesh
2170 The copy of current mesh with subdivided faces.
2171 """
2172 # subdivide vertex attributes
2173 visual = None
2174 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == (
2175 len(self.vertices),
2176 2,
2177 ):
2178 # uv coords divided along with vertices
2179 vertices_faces = remesh.subdivide_to_size(
2180 vertices=np.hstack((self.vertices, self.visual.uv)),
2181 faces=self.faces,
2182 max_edge=max_edge,
2183 max_iter=max_iter,
2184 return_index=return_index,
2185 )
2186 # unpack result
2187 if return_index:
2188 vertices, faces, final_index = vertices_faces
2189 else:
2190 vertices, faces = vertices_faces
2192 # get a copy of the current visuals
2193 visual = self.visual.copy()
2195 # separate uv coords and vertices
2196 vertices, visual.uv = vertices[:, :3], vertices[:, 3:]
2198 else:
2199 # uv coords divided along with vertices
2200 vertices_faces = remesh.subdivide_to_size(
2201 vertices=self.vertices,
2202 faces=self.faces,
2203 max_edge=max_edge,
2204 max_iter=max_iter,
2205 return_index=return_index,
2206 )
2207 # unpack result
2208 if return_index:
2209 vertices, faces, final_index = vertices_faces
2210 else:
2211 vertices, faces = vertices_faces
2213 # create a new mesh
2214 result = Trimesh(vertices=vertices, faces=faces, visual=visual, process=False)
2216 if return_index:
2217 return result, final_index
2219 return result
2221 def subdivide_loop(self, iterations: Integer | None = None) -> "Trimesh":
2222 """
2223 Subdivide a mesh by dividing each triangle into four
2224 triangles and approximating their smoothed surface
2225 using loop subdivision. Loop subdivision often looks
2226 better on triangular meshes than catmul-clark, which
2227 operates primarily on quads.
2229 Parameters
2230 ------------
2231 iterations : int
2232 Number of iterations to run subdivision.
2233 multibody : bool
2234 If True will try to subdivide for each submesh
2236 Returns
2237 ------------
2238 mesh: trimesh.Trimesh
2239 The copy of current mesh with subdivided faces.
2240 """
2241 # perform subdivision for one mesh
2242 new_vertices, new_faces = remesh.subdivide_loop(
2243 vertices=self.vertices, faces=self.faces, iterations=iterations
2244 )
2245 return Trimesh(vertices=new_vertices, faces=new_faces, process=False)
2247 @property
2248 def smooth_shaded(self) -> "Trimesh":
2249 """
2250 Smooth shading in OpenGL relies on which vertices are shared,
2251 this function will disconnect regions above an angle threshold
2252 and return a non-watertight version which will look better
2253 in an OpenGL rendering context.
2255 If you would like to use non-default arguments see `graph.smooth_shade`.
2257 Returns
2258 ---------
2259 smooth_shaded : trimesh.Trimesh
2260 Non watertight version of current mesh.
2261 """
2262 # key this also by the visual properties
2263 # but store it in the mesh cache
2264 self.visual._verify_hash()
2265 cache = self.visual._cache
2266 # needs to be dumped whenever visual or mesh changes
2267 key = f"smooth_shaded_{hash(self.visual)}_{hash(self)}"
2268 if key in cache:
2269 return cache[key]
2270 smooth = graph.smooth_shade(self)
2271 # store it in the mesh cache which dumps when vertices change
2272 cache[key] = smooth
2273 return smooth
2275 @property
2276 def visual(self) -> ColorVisuals | TextureVisuals | None:
2277 """
2278 Get the stored visuals for the current mesh.
2280 Returns
2281 -------------
2282 visual : ColorVisuals or TextureVisuals
2283 Contains visual information about the mesh
2284 """
2285 if hasattr(self, "_visual"):
2286 return self._visual
2287 return None
2289 @visual.setter
2290 def visual(self, value: ColorVisuals | TextureVisuals | None) -> None:
2291 """
2292 When setting a visual object, always make sure
2293 that `visual.mesh` points back to the source mesh.
2295 Parameters
2296 --------------
2297 visual : ColorVisuals or TextureVisuals
2298 Contains visual information about the mesh
2299 """
2300 if value is None:
2301 value = ColorVisuals()
2302 value.mesh = self
2303 self._visual = value
2305 def section(
2306 self, plane_normal: ArrayLike, plane_origin: ArrayLike, **kwargs
2307 ) -> Path3D | None:
2308 """
2309 Returns a 3D cross section of the current mesh and a plane
2310 defined by origin and normal.
2312 Parameters
2313 ------------
2314 plane_normal : (3,) float
2315 Normal vector of section plane.
2316 plane_origin : (3, ) float
2317 Point on the cross section plane.
2319 Returns
2320 ---------
2321 intersections
2322 Curve of intersection or None if it was not hit by plane.
2323 """
2324 # turn line segments into Path2D/Path3D objects
2325 from .path.exchange.misc import lines_to_path
2326 from .path.path import Path3D
2328 # return a single cross section in 3D
2329 lines, _face_index = intersections.mesh_plane(
2330 mesh=self,
2331 plane_normal=plane_normal,
2332 plane_origin=plane_origin,
2333 return_faces=True,
2334 **kwargs,
2335 )
2337 # if the section didn't hit the mesh return None
2338 if len(lines) == 0:
2339 return None
2341 # otherwise load the line segments into the keyword arguments
2342 # for a Path3D object.
2343 path = lines_to_path(lines)
2345 # add the face index info into metadata
2346 # path.metadata["face_index"] = face_index
2348 return Path3D(**path)
2350 def section_multiplane(
2351 self,
2352 plane_origin: ArrayLike,
2353 plane_normal: ArrayLike,
2354 heights: ArrayLike,
2355 ) -> list[Path2D | None]:
2356 """
2357 Return multiple parallel cross sections of the current
2358 mesh in 2D.
2360 Parameters
2361 ------------
2362 plane_origin : (3, ) float
2363 Point on the cross section plane
2364 plane_normal : (3) float
2365 Normal vector of section plane
2366 heights : (n, ) float
2367 Each section is offset by height along
2368 the plane normal.
2370 Returns
2371 ---------
2372 paths : (n, ) Path2D or None
2373 2D cross sections at specified heights.
2374 path.metadata['to_3D'] contains transform
2375 to return 2D section back into 3D space.
2376 """
2377 # turn line segments into Path2D/Path3D objects
2378 from .exchange.load import load_path
2380 # do a multiplane intersection
2381 lines, transforms, faces = intersections.mesh_multiplane(
2382 mesh=self,
2383 plane_normal=plane_normal,
2384 plane_origin=plane_origin,
2385 heights=heights,
2386 )
2388 # turn the line segments into Path2D objects
2389 paths = [None] * len(lines)
2390 for i, f, segments, T in zip(range(len(lines)), faces, lines, transforms):
2391 if len(segments) > 0:
2392 paths[i] = load_path(segments, metadata={"to_3D": T, "face_index": f})
2393 return paths
2395 def slice_plane(
2396 self,
2397 plane_origin: ArrayLike,
2398 plane_normal: ArrayLike,
2399 cap: bool = False,
2400 face_index: ArrayLike | None = None,
2401 **kwargs,
2402 ) -> "Trimesh":
2403 """
2404 Slice the mesh with a plane, returning a new mesh that is the
2405 portion of the original mesh to the positive normal side of the plane
2407 plane_origin : (3,) float
2408 Point on plane to intersect with mesh
2409 plane_normal : (3,) float
2410 Normal vector of plane to intersect with mesh
2411 cap : bool
2412 If True, cap the result with a triangulated polygon
2413 face_index : ((m,) int)
2414 Indexes of mesh.faces to slice. When no mask is
2415 provided, the default is to slice all faces.
2417 Returns
2418 ---------
2419 new_mesh: trimesh.Trimesh or None
2420 Subset of current mesh that intersects the half plane
2421 to the positive normal side of the plane
2422 """
2424 # return a new mesh
2425 new_mesh = intersections.slice_mesh_plane(
2426 mesh=self,
2427 plane_normal=plane_normal,
2428 plane_origin=plane_origin,
2429 cap=cap,
2430 face_index=face_index,
2431 **kwargs,
2432 )
2434 return new_mesh
2436 def unwrap(self, image=None) -> "Trimesh":
2437 """
2438 Returns a Trimesh object equivalent to the current mesh where
2439 the vertices have been assigned uv texture coordinates. Vertices
2440 may be split into as many as necessary by the unwrapping
2441 algorithm, depending on how many uv maps they appear in.
2443 Requires `pip install xatlas`
2445 Parameters
2446 ------------
2447 image : None or PIL.Image
2448 Image to assign to the material
2450 Returns
2451 --------
2452 unwrapped : trimesh.Trimesh
2453 Mesh with unwrapped uv coordinates
2454 """
2455 import xatlas
2457 vmap, faces, uv = xatlas.parametrize(self.vertices, self.faces)
2459 result = Trimesh(
2460 vertices=self.vertices[vmap],
2461 faces=faces,
2462 visual=TextureVisuals(uv=uv, image=image),
2463 process=False,
2464 )
2466 # run additional checks for unwrapping
2467 if tol.strict:
2468 # check the export object to make sure we didn't
2469 # move the indices around on creation
2470 assert np.allclose(result.visual.uv, uv)
2471 assert np.allclose(result.faces, faces)
2472 assert np.allclose(result.vertices, self.vertices[vmap])
2473 # check to make sure indices are still the
2474 # same order after we've exported to OBJ
2475 export = result.export(file_type="obj")
2476 uv_recon = np.array(
2477 [L[3:].split() for L in str.splitlines(export) if L.startswith("vt ")],
2478 dtype=float64,
2479 )
2480 assert np.allclose(uv_recon, uv)
2481 v_recon = np.array(
2482 [L[2:].split() for L in str.splitlines(export) if L.startswith("v ")],
2483 dtype=float64,
2484 )
2485 assert np.allclose(v_recon, self.vertices[vmap])
2487 return result
2489 @cache_decorator
2490 def convex_hull(self) -> "Trimesh":
2491 """
2492 Returns a Trimesh object representing the convex hull of
2493 the current mesh.
2495 Returns
2496 --------
2497 convex : trimesh.Trimesh
2498 Mesh of convex hull of current mesh
2499 """
2500 return convex.convex_hull(self)
2502 def sample(
2503 self,
2504 count: Integer,
2505 return_index: bool = False,
2506 face_weight: NDArray[float64] | None = None,
2507 ):
2508 """
2509 Return random samples distributed across the
2510 surface of the mesh
2512 Parameters
2513 ------------
2514 count : int
2515 Number of points to sample
2516 return_index : bool
2517 If True will also return the index of which face each
2518 sample was taken from.
2519 face_weight : None or len(mesh.faces) float
2520 Weight faces by a factor other than face area.
2521 If None will be the same as face_weight=mesh.area
2523 Returns
2524 ---------
2525 samples : (count, 3) float
2526 Points on surface of mesh
2527 face_index : (count, ) int
2528 Index of self.faces
2529 """
2530 samples, index = sample.sample_surface(
2531 mesh=self, count=count, face_weight=face_weight
2532 )
2533 if return_index:
2534 return samples, index
2535 return samples
2537 def remove_unreferenced_vertices(self) -> None:
2538 """
2539 Remove all vertices in the current mesh which are not
2540 referenced by a face.
2541 """
2542 referenced = np.zeros(len(self.vertices), dtype=bool)
2543 referenced[self.faces] = True
2545 inverse = np.zeros(len(self.vertices), dtype=int64)
2546 inverse[referenced] = np.arange(referenced.sum())
2548 self.update_vertices(mask=referenced, inverse=inverse)
2550 def unmerge_vertices(self) -> None:
2551 """
2552 Removes all face references so that every face contains
2553 three unique vertex indices and no faces are adjacent.
2554 """
2555 # new faces are incrementing so every vertex is unique
2556 faces = np.arange(len(self.faces) * 3, dtype=int64).reshape((-1, 3))
2558 # use update_vertices to apply mask to
2559 # all properties that are per-vertex
2560 self.update_vertices(self.faces.reshape(-1))
2561 # set faces to incrementing indexes
2562 self.faces = faces
2563 # keep face normals as the haven't changed
2564 self._cache.clear(exclude=["face_normals"])
2566 def apply_transform(self, matrix: ArrayLike) -> Self:
2567 """
2568 Transform mesh by a homogeneous transformation matrix.
2570 Does the bookkeeping to avoid recomputing things so this function
2571 should be used rather than directly modifying self.vertices
2572 if possible.
2574 Parameters
2575 ------------
2576 matrix : (4, 4) float
2577 Homogeneous transformation matrix
2578 """
2579 # get c-order float64 matrix
2580 matrix = np.asanyarray(matrix, order="C", dtype=float64)
2582 # only support homogeneous transformations
2583 if matrix.shape != (4, 4):
2584 raise ValueError("Transformation matrix must be (4, 4)!")
2586 # exit early if we've been passed an identity matrix
2587 # np.allclose is surprisingly slow so do this test
2588 elif util.allclose(matrix, _IDENTITY4, 1e-8):
2589 return self
2591 # new vertex positions
2592 new_vertices = transformations.transform_points(self.vertices, matrix=matrix)
2594 # check to see if the matrix has rotation
2595 # rather than just translation
2596 has_rotation = not util.allclose(matrix[:3, :3], _IDENTITY3, atol=1e-6)
2598 # transform overridden center of mass
2599 if "center_mass" in self._data:
2600 center_mass = [self._data["center_mass"]]
2601 self.center_mass = transformations.transform_points(
2602 center_mass,
2603 matrix,
2604 )[0]
2606 # preserve face normals if we have them stored
2607 if has_rotation and "face_normals" in self._cache:
2608 # transform face normals by rotation component
2609 self._cache.cache["face_normals"] = util.unitize(
2610 transformations.transform_points(
2611 self.face_normals, matrix=matrix, translate=False
2612 )
2613 )
2615 # preserve vertex normals if we have them stored
2616 if has_rotation and "vertex_normals" in self._cache:
2617 self._cache.cache["vertex_normals"] = util.unitize(
2618 transformations.transform_points(
2619 self.vertex_normals, matrix=matrix, translate=False
2620 )
2621 )
2623 # if transformation flips winding of triangles
2624 if has_rotation and transformations.flips_winding(matrix):
2625 log.debug("transform flips winding")
2626 # fliplr will make array non C contiguous
2627 # which will cause hashes to be more
2628 # expensive than necessary so wrap
2629 self.faces = np.ascontiguousarray(np.fliplr(self.faces))
2631 # assign the new values
2632 self.vertices = new_vertices
2634 # preserve normals and topology in cache
2635 # while dumping everything else
2636 self._cache.clear(
2637 exclude={
2638 "face_normals", # transformed by us
2639 "vertex_normals", # also transformed by us
2640 "face_adjacency", # topological
2641 "face_adjacency_edges",
2642 "face_adjacency_unshared",
2643 "edges",
2644 "edges_face",
2645 "edges_sorted",
2646 "edges_unique",
2647 "edges_unique_idx",
2648 "edges_unique_inverse",
2649 "edges_sparse",
2650 "body_count",
2651 "faces_unique_edges",
2652 "euler_number",
2653 }
2654 )
2655 # set the cache ID with the current hash value
2656 self._cache.id_set()
2657 return self
2659 def voxelized(self, pitch: Floating | None, method: str = "subdivide", **kwargs):
2660 """
2661 Return a VoxelGrid object representing the current mesh
2662 discretized into voxels at the specified pitch
2664 Parameters
2665 ------------
2666 pitch : float
2667 The edge length of a single voxel
2668 method: implementation key. See `trimesh.voxel.creation.voxelizers`
2669 **kwargs: additional kwargs passed to the specified implementation.
2671 Returns
2672 ----------
2673 voxelized : VoxelGrid object
2674 Representing the current mesh
2675 """
2676 from .voxel import creation
2678 return creation.voxelize(mesh=self, pitch=pitch, method=method, **kwargs)
2680 def simplify_quadric_decimation(
2681 self,
2682 percent: Floating | None = None,
2683 face_count: Integer | None = None,
2684 aggression: Integer | None = None,
2685 ) -> "Trimesh":
2686 """
2687 A thin wrapper around `pip install fast-simplification`.
2689 Parameters
2690 -----------
2691 percent
2692 A number between 0.0 and 1.0 for how much
2693 face_count
2694 Target number of faces desired in the resulting mesh.
2695 aggression
2696 An integer between `0` and `10`, the scale being roughly
2697 `0` is "slow and good" and `10` being "fast and bad."
2699 Returns
2700 ---------
2701 simple : trimesh.Trimesh
2702 Simplified version of mesh.
2703 """
2704 from fast_simplification import simplify
2706 # create keyword arguments as dict so we can filter out `None`
2707 # values as the C wrapper as of writing is not happy with `None`
2708 # and requires they be omitted from the constructor
2709 kwargs = {
2710 "target_count": face_count,
2711 "target_reduction": percent,
2712 "agg": aggression,
2713 }
2715 # todo : one could take the `return_collapses=True` array and use it to
2716 # apply the same simplification to the visual info
2717 vertices, faces = simplify(
2718 points=self.vertices.view(np.ndarray),
2719 triangles=self.faces.view(np.ndarray),
2720 **{k: v for k, v in kwargs.items() if v is not None},
2721 )
2723 return Trimesh(vertices=vertices, faces=faces)
2725 def outline(self, face_ids: NDArray[int64] | None = None, **kwargs) -> Path3D:
2726 """
2727 Given a list of face indexes find the outline of those
2728 faces and return it as a Path3D.
2730 The outline is defined here as every edge which is only
2731 included by a single triangle.
2733 Note that this implies a non-watertight mesh as the
2734 outline of a watertight mesh is an empty path.
2736 Parameters
2737 ------------
2738 face_ids : (n, ) int
2739 Indices to compute the outline of.
2740 If None, outline of full mesh will be computed.
2741 **kwargs: passed to Path3D constructor
2743 Returns
2744 ----------
2745 path : Path3D
2746 Curve in 3D of the outline
2747 """
2748 from .path.exchange.misc import faces_to_path
2750 return Path3D(**faces_to_path(self, face_ids, **kwargs))
2752 def projected(self, normal: ArrayLike, **kwargs) -> Path2D:
2753 """
2754 Project a mesh onto a plane and then extract the
2755 polygon that outlines the mesh projection on that
2756 plane.
2758 Parameters
2759 ----------
2760 normal : (3,) float
2761 Normal to extract flat pattern along
2762 origin : None or (3,) float
2763 Origin of plane to project mesh onto
2764 ignore_sign : bool
2765 Allow a projection from the normal vector in
2766 either direction: this provides a substantial speedup
2767 on watertight meshes where the direction is irrelevant
2768 but if you have a triangle soup and want to discard
2769 backfaces you should set this to False.
2770 rpad : float
2771 Proportion to pad polygons by before unioning
2772 and then de-padding result by to avoid zero-width gaps.
2773 apad : float
2774 Absolute padding to pad polygons by before unioning
2775 and then de-padding result by to avoid zero-width gaps.
2776 tol_dot : float
2777 Tolerance for discarding on-edge triangles.
2778 precise : bool
2779 Use the precise projection computation using shapely.
2780 precise_eps : float
2781 Tolerance for precise triangle checks.
2783 Returns
2784 ----------
2785 projected : trimesh.path.Path2D
2786 Outline of source mesh
2787 """
2788 from .exchange.load import load_path
2789 from .path import Path2D
2790 from .path.polygons import projected
2792 projection = projected(mesh=self, normal=normal, **kwargs)
2793 if projection is None:
2794 return Path2D()
2795 return load_path(projection)
2797 @cache_decorator
2798 def area(self) -> float64:
2799 """
2800 Summed area of all triangles in the current mesh.
2802 Returns
2803 ---------
2804 area : float
2805 Surface area of mesh
2806 """
2807 area = self.area_faces.sum()
2808 return area
2810 @cache_decorator
2811 def area_faces(self) -> NDArray[float64]:
2812 """
2813 The area of each face in the mesh.
2815 Returns
2816 ---------
2817 area_faces : (n, ) float
2818 Area of each face
2819 """
2820 return triangles.area(crosses=self.triangles_cross)
2822 @cache_decorator
2823 def mass_properties(self) -> MassProperties:
2824 """
2825 Returns the mass properties of the current mesh.
2827 Assumes uniform density, and result is probably garbage if mesh
2828 isn't watertight.
2830 Returns
2831 ----------
2832 properties : dict
2833 With keys:
2834 'volume' : in global units^3
2835 'mass' : From specified density
2836 'density' : Included again for convenience (same as kwarg density)
2837 'inertia' : Taken at the center of mass and aligned with global
2838 coordinate system
2839 'center_mass' : Center of mass location, in global coordinate system
2840 """
2841 # if the density or center of mass was overridden they will be put into data
2842 density = self._data.data.get("density", None)
2843 center_mass = self._data.data.get("center_mass", None)
2844 return triangles.mass_properties(
2845 triangles=self.triangles,
2846 crosses=self.triangles_cross,
2847 density=density,
2848 center_mass=center_mass,
2849 skip_inertia=False,
2850 )
2852 def invert(self) -> Self:
2853 """
2854 Invert the mesh in-place by reversing the winding of every
2855 face and negating normals without dumping the cache.
2857 Alters `self.faces` by reversing columns, and negating
2858 `self.face_normals` and `self.vertex_normals`.
2859 """
2860 with self._cache:
2861 if "face_normals" in self._cache:
2862 self.face_normals = self._cache["face_normals"] * -1.0
2863 if "vertex_normals" in self._cache:
2864 self.vertex_normals = self._cache["vertex_normals"] * -1.0
2865 # fliplr makes array non-contiguous so cache checks slow
2866 self.faces = np.ascontiguousarray(np.fliplr(self.faces))
2867 # save our normals
2868 self._cache.clear(exclude=["face_normals", "vertex_normals"])
2870 return self
2872 def scene(self, **kwargs) -> Scene:
2873 """
2874 Returns a Scene object containing the current mesh.
2876 Returns
2877 ---------
2878 scene : trimesh.scene.scene.Scene
2879 Contains just the current mesh
2880 """
2881 return Scene(self, **kwargs)
2883 def show(
2884 self,
2885 viewer: ViewerType = None,
2886 **kwargs,
2887 ) -> Scene:
2888 """
2889 Render the mesh in an opengl window. Requires pyglet.
2891 Parameters
2892 ------------
2893 viewer : ViewerType
2894 What kind of viewer to use, such as
2895 `gl` to open a pyglet window
2896 `jupyter` for a jupyter notebook
2897 `marimo'` for a marimo notebook
2898 None for a "best guess"
2899 smooth : bool
2900 Run smooth shading on mesh or not,
2901 large meshes will be slow
2903 Returns
2904 -----------
2905 scene : trimesh.scene.Scene
2906 Scene with current mesh in it
2907 """
2908 scene = self.scene()
2909 return scene.show(viewer=viewer, **kwargs)
2911 def submesh(
2912 self,
2913 faces_sequence: Sequence[ArrayLike],
2914 only_watertight: bool = False,
2915 repair: bool = False,
2916 **kwargs,
2917 ) -> "Trimesh | list[Trimesh]":
2918 """
2919 Return a subset of the mesh.
2921 Parameters
2922 ------------
2923 faces_sequence : sequence (m, ) int
2924 Face indices of mesh
2925 only_watertight : bool
2926 Only return submeshes which are watertight
2927 repair
2928 Try to repair the submesh if it is not watertight
2929 append : bool
2930 Return a single mesh which has the faces appended.
2931 if this flag is set, only_watertight is ignored
2933 Returns
2934 ---------
2935 submesh : Trimesh or (n,) Trimesh
2936 Single mesh if `append` or list of submeshes
2937 """
2938 return util.submesh(
2939 mesh=self,
2940 faces_sequence=faces_sequence,
2941 only_watertight=only_watertight,
2942 repair=repair,
2943 **kwargs,
2944 )
2946 @cache_decorator
2947 def identifier(self) -> NDArray[float64]:
2948 """
2949 Return a float vector which is unique to the mesh
2950 and is robust to rotation and translation.
2952 Returns
2953 -----------
2954 identifier : (7,) float
2955 Identifying properties of the current mesh
2956 """
2957 return comparison.identifier_simple(self)
2959 @cache_decorator
2960 def identifier_hash(self) -> str:
2961 """
2962 A hash of the rotation invariant identifier vector.
2964 Returns
2965 ---------
2966 hashed : str
2967 Hex string of the SHA256 hash from
2968 the identifier vector at hand-tuned sigfigs.
2969 """
2970 return comparison.identifier_hash(self.identifier)
2972 def export(
2973 self,
2974 file_obj: Loadable = None,
2975 file_type: str | None = None,
2976 **kwargs,
2977 ) -> dict | bytes | str:
2978 """
2979 Export the current mesh to a file object.
2980 If file_obj is a filename, file will be written there.
2982 Supported formats are stl, off, ply, collada, json,
2983 dict, glb, dict64, msgpack.
2985 Parameters
2986 ------------
2987 file_obj : open writeable file object
2988 str, file name where to save the mesh
2989 None, return the export blob
2990 file_type : str
2991 Which file type to export as, if `file_name`
2992 is passed this is not required.
2994 Returns
2995 ----------
2996 exported : bytes or str
2997 Result of exporter
2998 """
2999 return export_mesh(mesh=self, file_obj=file_obj, file_type=file_type, **kwargs)
3001 def to_dict(self) -> dict[str, str | list[list[float]] | list[list[int]]]:
3002 """
3003 Return a dictionary representation of the current mesh
3004 with keys that can be used as the kwargs for the
3005 Trimesh constructor and matches the schema in:
3006 `trimesh/resources/schema/primitive/trimesh.schema.json`
3008 Returns
3009 ----------
3010 result : dict
3011 Matches schema and Trimesh constructor.
3012 """
3013 return {
3014 "kind": "trimesh",
3015 "vertices": self.vertices.tolist(),
3016 "faces": self.faces.tolist(),
3017 }
3019 def convex_decomposition(self, **kwargs) -> list["Trimesh"]:
3020 """
3021 Compute an approximate convex decomposition of a mesh
3022 using `pip install pyVHACD`.
3024 Returns
3025 -------
3026 meshes
3027 List of convex meshes that approximate the original
3028 **kwargs : VHACD keyword arguments
3029 """
3030 return [
3031 Trimesh(**kwargs)
3032 for kwargs in decomposition.convex_decomposition(self, **kwargs)
3033 ]
3035 def union(
3036 self,
3037 other: "Trimesh | Sequence[Trimesh]",
3038 engine: BooleanEngineType = None,
3039 check_volume: bool = True,
3040 **kwargs,
3041 ) -> "Trimesh":
3042 """
3043 Boolean union between this mesh and other meshes.
3045 Parameters
3046 ------------
3047 other : Trimesh or (n, ) Trimesh
3048 Other meshes to union
3049 engine
3050 Which backend to use, the default
3051 recommendation is: `pip install manifold3d`.
3052 check_volume
3053 Raise an error if not all meshes are watertight
3054 positive volumes. Advanced users may want to ignore
3055 this check as it is expensive.
3056 kwargs
3057 Passed through to the `engine`.
3059 Returns
3060 ---------
3061 union : trimesh.Trimesh
3062 Union of self and other Trimesh objects
3063 """
3064 return boolean.union(
3065 meshes=util.chain(self, other),
3066 engine=engine,
3067 check_volume=check_volume,
3068 **kwargs,
3069 )
3071 def difference(
3072 self,
3073 other: "Trimesh | Sequence[Trimesh]",
3074 engine: BooleanEngineType = None,
3075 check_volume: bool = True,
3076 **kwargs,
3077 ) -> "Trimesh":
3078 """
3079 Boolean difference between this mesh and other meshes.
3081 Parameters
3082 ------------
3083 other
3084 One or more meshes to difference with the current mesh.
3085 engine
3086 Which backend to use, the default
3087 recommendation is: `pip install manifold3d`.
3088 check_volume
3089 Raise an error if not all meshes are watertight
3090 positive volumes. Advanced users may want to ignore
3091 this check as it is expensive.
3092 kwargs
3093 Passed through to the `engine`.
3095 Returns
3096 ---------
3097 difference : trimesh.Trimesh
3098 Difference between self and other Trimesh objects
3099 """
3100 return boolean.difference(
3101 meshes=util.chain(self, other),
3102 engine=engine,
3103 check_volume=check_volume,
3104 **kwargs,
3105 )
3107 def intersection(
3108 self,
3109 other: "Trimesh | Sequence[Trimesh]",
3110 engine: BooleanEngineType = None,
3111 check_volume: bool = True,
3112 **kwargs,
3113 ) -> "Trimesh":
3114 """
3115 Boolean intersection between this mesh and other meshes.
3117 Parameters
3118 ------------
3119 other : trimesh.Trimesh, or list of trimesh.Trimesh objects
3120 Meshes to calculate intersections with
3121 engine
3122 Which backend to use, the default
3123 recommendation is: `pip install manifold3d`.
3124 check_volume
3125 Raise an error if not all meshes are watertight
3126 positive volumes. Advanced users may want to ignore
3127 this check as it is expensive.
3128 kwargs
3129 Passed through to the `engine`.
3131 Returns
3132 ---------
3133 intersection : trimesh.Trimesh
3134 Mesh of the volume contained by all passed meshes
3135 """
3136 return boolean.intersection(
3137 meshes=util.chain(self, other),
3138 engine=engine,
3139 check_volume=check_volume,
3140 **kwargs,
3141 )
3143 def contains(self, points: ArrayLike) -> NDArray[np.bool_]:
3144 """
3145 Given an array of points determine whether or not they
3146 are inside the mesh. This raises an error if called on a
3147 non-watertight mesh.
3149 Parameters
3150 ------------
3151 points : (n, 3) float
3152 Points in cartesian space
3154 Returns
3155 ---------
3156 contains : (n, ) bool
3157 Whether or not each point is inside the mesh
3158 """
3159 return self.ray.contains_points(points)
3161 @cache_decorator
3162 def face_angles(self) -> NDArray[float64]:
3163 """
3164 Returns the angle at each vertex of a face.
3166 Returns
3167 --------
3168 angles : (len(self.faces), 3) float
3169 Angle at each vertex of a face
3170 """
3171 return triangles.angles(self.triangles)
3173 @cache_decorator
3174 def face_angles_sparse(self) -> coo_matrix:
3175 """
3176 A sparse matrix representation of the face angles.
3178 Returns
3179 ----------
3180 sparse : scipy.sparse.coo_matrix
3181 Float sparse matrix with with shape:
3182 (len(self.vertices), len(self.faces))
3183 """
3184 angles = curvature.face_angles_sparse(self)
3185 return angles
3187 @cache_decorator
3188 def vertex_defects(self) -> NDArray[float64]:
3189 """
3190 Return the vertex defects, or (2*pi) minus the sum of the angles
3191 of every face that includes that vertex.
3193 If a vertex is only included by coplanar triangles, this
3194 will be zero. For convex regions this is positive, and
3195 concave negative.
3197 Returns
3198 --------
3199 vertex_defect : (len(self.vertices), ) float
3200 Vertex defect at the every vertex
3201 """
3202 defects = curvature.vertex_defects(self)
3203 return defects
3205 @cache_decorator
3206 def vertex_degree(self) -> NDArray[int64]:
3207 """
3208 Return the number of faces each vertex is included in.
3210 Returns
3211 ----------
3212 degree : (len(self.vertices), ) int
3213 Number of faces each vertex is included in
3214 """
3215 # get degree through sparse matrix
3216 degree = np.array(self.faces_sparse.sum(axis=1)).flatten()
3217 return degree
3219 @cache_decorator
3220 def face_adjacency_tree(self) -> Index:
3221 """
3222 An R-tree of face adjacencies.
3224 Returns
3225 --------
3226 tree
3227 Where each edge in self.face_adjacency has a
3228 rectangular cell
3229 """
3230 # the (n,6) interleaved bounding box for every line segment
3231 return util.bounds_tree(
3232 np.column_stack(
3233 (
3234 self.vertices[self.face_adjacency_edges].min(axis=1),
3235 self.vertices[self.face_adjacency_edges].max(axis=1),
3236 )
3237 )
3238 )
3240 def copy(self, include_cache: bool = False, include_visual: bool = True) -> "Trimesh":
3241 """
3242 Safely return a copy of the current mesh.
3244 By default, copied meshes will have emptied cache
3245 to avoid memory issues and so may be slow on initial
3246 operations until caches are regenerated.
3248 Current object will *never* have its cache cleared.
3250 Parameters
3251 ------------
3252 include_cache : bool
3253 If True, will shallow copy cached data to new mesh
3254 include_visual : bool
3255 If True, will copy visual information
3257 Returns
3258 ---------
3259 copied : trimesh.Trimesh
3260 Copy of current mesh
3261 """
3262 # start with an empty mesh
3263 copied = Trimesh()
3264 # always deepcopy vertex and face data
3265 copied._data.data = deepcopy(self._data.data)
3267 if include_visual:
3268 # copy visual information
3269 copied.visual = self.visual.copy()
3271 copied.vertex_attributes.update(
3272 {k: deepcopy(v) for k, v in self.vertex_attributes.items()}
3273 )
3274 copied.face_attributes.update(
3275 {k: deepcopy(v) for k, v in self.face_attributes.items()}
3276 )
3278 # get metadata
3279 copied.metadata = deepcopy(self.metadata)
3281 # make sure cache ID is set initially
3282 copied._cache.verify()
3284 if include_cache:
3285 # shallow copy cached items into the new cache
3286 # since the data didn't change here when the
3287 # data in the new mesh is changed these items
3288 # will be dumped in the new mesh but preserved
3289 # in the original mesh
3290 copied._cache.cache.update(self._cache.cache)
3292 return copied
3294 def __deepcopy__(self, *args) -> "Trimesh":
3295 # interpret deep copy as "get rid of cached data"
3296 return self.copy(include_cache=False)
3298 def __copy__(self, *args) -> "Trimesh":
3299 # interpret shallow copy as "keep cached data"
3300 return self.copy(include_cache=True)
3302 def eval_cached(self, statement: str, *args) -> Any:
3303 """
3304 DEPRECATED: call `eval` directly instead.
3306 Evaluate a statement and cache the result before returning.
3308 Parameters
3309 ------------
3310 statement : str
3311 Statement of valid python code
3312 *args : list
3313 Available inside statement as args[0], etc
3315 Returns
3316 -----------
3317 result : result of running eval on statement with args
3319 Examples
3320 -----------
3321 r = mesh.eval_cached('np.dot(self.vertices, args[0])', [0, 0, 1])
3322 """
3323 import warnings
3325 warnings.warn(
3326 "`Trimesh.eval_cached` is deprecated "
3327 + "and will be removed in a future release. "
3328 + "call `eval` directly if you need this behavior.",
3329 category=DeprecationWarning,
3330 stacklevel=2,
3331 )
3333 # store this by the combined hash of statement and args
3334 hashable = [hash(statement)]
3335 hashable.extend(hash(a) for a in args)
3337 key = f"eval_cached_{hash(tuple(hashable))}"
3339 if key in self._cache:
3340 return self._cache[key]
3342 result = eval(statement)
3343 self._cache[key] = result
3344 return result
3346 def __add__(self, other: "Trimesh") -> "Trimesh":
3347 """
3348 Concatenate the mesh with another mesh.
3350 Parameters
3351 ------------
3352 other : trimesh.Trimesh object
3353 Mesh to be concatenated with self
3355 Returns
3356 ----------
3357 concat : trimesh.Trimesh
3358 Mesh object of combined result
3359 """
3360 concat = util.concatenate(self, other)
3361 return concat