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