Coverage for trimesh/base.py: 93%
818 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-24 01:00 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-24 01:00 +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 mapping shared edges to face adjacency indices.
1527 The tree contains only ``face_adjacency_edges``. Sort each pair of
1528 vertex indices before querying. Edges outside this set, including
1529 boundary edges, can return a nearest neighbor index. Check that the
1530 query distance is close to zero before using the index to access
1531 ``face_adjacency``. Distances are measured between vertex index pairs.
1533 Returns
1534 ------------
1535 tree : scipy.spatial.cKDTree
1536 Tree of sorted vertex index pairs for shared edges.
1538 Examples
1539 --------
1540 Look up a shared edge and a boundary edge, keeping only the match:
1542 >>> import numpy as np
1543 >>> import trimesh
1544 >>> mesh = trimesh.Trimesh(
1545 ... vertices=[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],
1546 ... faces=[[0, 1, 2], [0, 2, 3]],
1547 ... process=False,
1548 ... )
1549 >>> edges = np.sort([[2, 0], [0, 1]], axis=1)
1550 >>> distance, index = mesh.face_adjacency_edges_tree.query(edges)
1551 >>> matched = distance < 1e-12
1552 >>> mesh.face_adjacency[index[matched]]
1553 array([[0, 1]])
1554 """
1555 return cKDTree(self.face_adjacency_edges)
1557 @cache_decorator
1558 def face_adjacency_angles(self) -> NDArray[float64]:
1559 """
1560 Return the unsigned angle between adjacent faces
1561 in radians.
1563 Note that if you want a signed angle you can easily
1564 use the `face_adjacency_convex` attribute to get a
1565 signed angle with advanced indexing:
1567 ```
1568 # get a sign per face_adacency pair from the "is it convex" boolean
1569 signs = np.array([-1.0, 1.0])[mesh.face_adjacency_convex.astype(np.int64)]
1571 # apply the signs to the angles
1572 angles = mesh.face_adjacency_angles * signs
1573 ```
1575 Returns
1576 --------
1577 adjacency_angle : (len(self.face_adjacency), ) float
1578 Unsigned angle between adjacent faces
1579 corresponding with `self.face_adjacency`
1580 """
1581 # get pairs of unit vectors for adjacent faces
1582 pairs = self.face_normals[self.face_adjacency]
1583 # find the angle between the pairs of vectors
1584 angles = geometry.vector_angle(pairs)
1585 return angles
1587 @cache_decorator
1588 def face_adjacency_projections(self) -> NDArray[float64]:
1589 """
1590 The projection of the non-shared vertex of a triangle onto
1591 its adjacent face
1593 Returns
1594 ----------
1595 projections : (len(self.face_adjacency), ) float
1596 Dot product of vertex
1597 onto plane of adjacent triangle.
1598 """
1599 projections = convex.adjacency_projections(self)
1600 return projections
1602 @cache_decorator
1603 def face_adjacency_convex(self) -> NDArray[np.bool_]:
1604 """
1605 Return faces which are adjacent and locally convex.
1607 What this means is that given faces A and B, the one vertex
1608 in B that is not shared with A, projected onto the plane of A
1609 has a projection that is zero or negative.
1611 Returns
1612 ----------
1613 are_convex : (len(self.face_adjacency), ) bool
1614 Face pairs that are locally convex
1615 """
1616 return self.face_adjacency_projections < tol.merge
1618 @cache_decorator
1619 def face_adjacency_unshared(self) -> NDArray[int64]:
1620 """
1621 Return the vertex index of the two vertices not in the shared
1622 edge between two adjacent faces
1624 Returns
1625 -----------
1626 vid_unshared : (len(mesh.face_adjacency), 2) int
1627 Indexes of mesh.vertices
1628 """
1629 return graph.face_adjacency_unshared(self)
1631 @cache_decorator
1632 def face_adjacency_radius(self) -> NDArray[float64]:
1633 """
1634 The approximate radius of a cylinder that fits inside adjacent faces.
1636 Returns
1637 ------------
1638 radii : (len(self.face_adjacency), ) float
1639 Approximate radius formed by triangle pair
1640 """
1641 radii, self._cache["face_adjacency_span"] = graph.face_adjacency_radius(mesh=self)
1642 return radii
1644 @cache_decorator
1645 def face_adjacency_span(self) -> NDArray[float64]:
1646 """
1647 The approximate perpendicular projection of the non-shared
1648 vertices in a pair of adjacent faces onto the shared edge of
1649 the two faces.
1651 Returns
1652 ------------
1653 span : (len(self.face_adjacency), ) float
1654 Approximate span between the non-shared vertices
1655 """
1656 _ = self.face_adjacency_radius
1657 return self._cache["face_adjacency_span"]
1659 @cache_decorator
1660 def integral_mean_curvature(self) -> float64:
1661 """
1662 The integral mean curvature, or the surface integral of the mean curvature.
1664 Returns
1665 ---------
1666 area : float
1667 Integral mean curvature of mesh
1668 """
1669 edges_length = np.linalg.norm(
1670 np.subtract(*self.vertices[self.face_adjacency_edges.T]), axis=1
1671 )
1672 # assign signs based on convex adjacency of face pairs
1673 signs = np.array([-1.0, 1.0])[self.face_adjacency_convex.astype(np.int64)]
1674 # adjust face adjacency angles with signs to reflect orientation
1675 angles = self.face_adjacency_angles * signs
1676 return (angles * edges_length).sum() * 0.5
1678 @cache_decorator
1679 def vertex_adjacency_graph(self) -> Graph:
1680 """
1681 Returns a networkx graph representing the vertices and their connections
1682 in the mesh.
1684 Returns
1685 ---------
1686 graph: networkx.Graph
1687 Graph representing vertices and edges between
1688 them where vertices are nodes and edges are edges
1690 Examples
1691 ----------
1692 This is useful for getting nearby vertices for a given vertex,
1693 potentially for some simple smoothing techniques.
1695 mesh = trimesh.primitives.Box()
1696 graph = mesh.vertex_adjacency_graph
1697 graph.neighbors(0)
1698 > [1, 2, 3, 4]
1699 """
1701 return graph.vertex_adjacency_graph(mesh=self)
1703 @cache_decorator
1704 def vertex_neighbors(self) -> list[list[int64]]:
1705 """
1706 The vertex neighbors of each vertex of the mesh, determined from
1707 the cached vertex_adjacency_graph, if already existent.
1709 Returns
1710 ----------
1711 vertex_neighbors : (len(self.vertices), ) int
1712 Represents immediate neighbors of each vertex along
1713 the edge of a triangle
1715 Examples
1716 ----------
1717 This is useful for getting nearby vertices for a given vertex,
1718 potentially for some simple smoothing techniques.
1720 >>> mesh = trimesh.primitives.Box()
1721 >>> mesh.vertex_neighbors[0]
1722 [1, 2, 3, 4]
1723 """
1724 return graph.neighbors(edges=self.edges_unique, max_index=len(self.vertices))
1726 @cache_decorator
1727 def is_winding_consistent(self) -> bool:
1728 """
1729 Does the mesh have consistent winding or not.
1730 A mesh with consistent winding has each shared edge
1731 going in an opposite direction from the other in the pair.
1733 Returns
1734 --------
1735 consistent : bool
1736 Is winding is consistent or not
1737 """
1738 if self.is_empty:
1739 return False
1740 # consistent winding check is populated into the cache by is_watertight
1741 _ = self.is_watertight
1742 return self._cache["is_winding_consistent"]
1744 @cache_decorator
1745 def is_watertight(self) -> bool:
1746 """
1747 Check if a mesh is watertight by making sure every edge is
1748 included in two faces.
1750 Returns
1751 ----------
1752 is_watertight : bool
1753 Is mesh watertight or not
1754 """
1755 if self.is_empty:
1756 return False
1757 watertight, winding = graph.is_watertight(
1758 edges=self.edges, edges_sorted=self.edges_sorted
1759 )
1760 self._cache["is_winding_consistent"] = winding
1761 return watertight
1763 @cache_decorator
1764 def is_volume(self) -> bool:
1765 """
1766 Check if a mesh has all the properties required to represent
1767 a valid volume, rather than just a surface.
1769 These properties include being watertight, having consistent
1770 winding and outward facing normals.
1772 Returns
1773 ---------
1774 valid
1775 Does the mesh represent a volume
1776 """
1777 return bool(
1778 self.is_watertight
1779 and self.is_winding_consistent
1780 and np.isfinite(self.center_mass).all()
1781 and self.volume > 0.0
1782 )
1784 @property
1785 def is_empty(self) -> bool:
1786 """
1787 Does the current mesh have data defined.
1789 Returns
1790 --------
1791 empty : bool
1792 If True, no data is set on the current mesh
1793 """
1794 return self._data.is_empty()
1796 @cache_decorator
1797 def is_convex(self) -> bool:
1798 """
1799 Check if a mesh is convex or not.
1801 Returns
1802 ----------
1803 is_convex: bool
1804 Is mesh convex or not
1805 """
1806 if self.is_empty:
1807 return False
1809 is_convex = bool(convex.is_convex(self))
1810 return is_convex
1812 @cache_decorator
1813 def kdtree(self) -> cKDTree:
1814 """
1815 Return a scipy.spatial.cKDTree of the vertices of the mesh.
1816 Not cached as this lead to observed memory issues and segfaults.
1818 Returns
1819 ---------
1820 tree : scipy.spatial.cKDTree
1821 Contains mesh.vertices
1822 """
1823 return cKDTree(self.vertices.view(np.ndarray))
1825 def nondegenerate_faces(self, height: Floating = tol.merge) -> NDArray[np.bool_]:
1826 """
1827 Identify degenerate faces (faces without 3 unique vertex indices)
1828 in the current mesh.
1830 Usage example for removing them:
1831 `mesh.update_faces(mesh.nondegenerate_faces())`
1833 If a height is specified, it will identify any face with a 2D oriented
1834 bounding box with one edge shorter than that height.
1836 If not specified, it will identify any face with a zero normal.
1838 Parameters
1839 ------------
1840 height : float
1841 If specified identifies faces with an oriented bounding
1842 box shorter than this on one side.
1844 Returns
1845 -------------
1846 nondegenerate : (len(self.faces), ) bool
1847 Mask that can be used to remove faces
1848 """
1849 return triangles.nondegenerate(
1850 self.triangles, areas=self.area_faces, height=height
1851 )
1853 @cache_decorator
1854 def facets(self) -> list[NDArray[int64]]:
1855 """
1856 Return a list of face indices for coplanar adjacent faces.
1858 Returns
1859 ---------
1860 facets : (n, ) sequence of (m, ) int
1861 Groups of indexes of self.faces
1862 """
1863 facets = graph.facets(self)
1864 return facets
1866 @cache_decorator
1867 def facets_area(self) -> NDArray[float64]:
1868 """
1869 Return an array containing the area of each facet.
1871 Returns
1872 ---------
1873 area : (len(self.facets), ) float
1874 Total area of each facet (group of faces)
1875 """
1876 # avoid thrashing the cache inside a loop
1877 area_faces = self.area_faces
1878 # sum the area of each group of faces represented by facets
1879 # use native python sum in tight loop as opposed to array.sum()
1880 # as in this case the lower function call overhead of
1881 # native sum provides roughly a 50% speedup
1882 areas = np.array([sum(area_faces[i]) for i in self.facets], dtype=float64)
1883 return areas
1885 @cache_decorator
1886 def facets_normal(self) -> NDArray[float64]:
1887 """
1888 Return the normal of each facet
1890 Returns
1891 ---------
1892 normals: (len(self.facets), 3) float
1893 A unit normal vector for each facet
1894 """
1895 if len(self.facets) == 0:
1896 return np.array([])
1898 area_faces = self.area_faces
1900 # the face index of the largest face in each facet
1901 index = np.array([i[area_faces[i].argmax()] for i in self.facets])
1902 # (n, 3) float, unit normal vectors of facet plane
1903 normals = self.face_normals[index]
1904 # (n, 3) float, points on facet plane
1905 origins = self.vertices[self.faces[:, 0][index]]
1906 # save origins in cache
1907 self._cache["facets_origin"] = origins
1909 return normals
1911 @cache_decorator
1912 def facets_origin(self) -> NDArray[float64]:
1913 """
1914 Return a point on the facet plane.
1916 Returns
1917 ------------
1918 origins : (len(self.facets), 3) float
1919 A point on each facet plane
1920 """
1921 _ = self.facets_normal
1922 return self._cache["facets_origin"]
1924 @cache_decorator
1925 def facets_boundary(self) -> list[NDArray[int64]]:
1926 """
1927 Return the edges which represent the boundary of each facet
1929 Returns
1930 ---------
1931 edges_boundary : sequence of (n, 2) int
1932 Indices of self.vertices
1933 """
1934 # make each row correspond to a single face
1935 edges = self.edges_sorted.reshape((-1, 6))
1936 # get the edges for each facet
1937 edges_facet = [edges[i].reshape((-1, 2)) for i in self.facets]
1938 edges_boundary = [i[grouping.group_rows(i, require_count=1)] for i in edges_facet]
1939 return edges_boundary
1941 @cache_decorator
1942 def facets_on_hull(self) -> NDArray[np.bool_]:
1943 """
1944 Find which facets of the mesh are on the convex hull.
1946 Returns
1947 ---------
1948 on_hull : (len(mesh.facets), ) bool
1949 is A facet on the meshes convex hull or not
1950 """
1951 # if no facets exit early
1952 if len(self.facets) == 0:
1953 return np.array([], dtype=bool)
1955 # facets plane, origin and normal
1956 normals = self.facets_normal
1957 origins = self.facets_origin
1959 # (n, 3) convex hull vertices
1960 convex = self.convex_hull.vertices.view(np.ndarray).copy()
1962 # boolean mask for which facets are on convex hull
1963 on_hull = np.zeros(len(self.facets), dtype=bool)
1965 for i, normal, origin in zip(range(len(normals)), normals, origins):
1966 # a facet plane is on the convex hull if every vertex
1967 # of the convex hull is behind that plane
1968 # which we are checking with dot products
1969 dot = np.dot(normal, (convex - origin).T)
1970 on_hull[i] = (dot < tol.merge).all()
1972 return on_hull
1974 def fix_normals(self, multibody: bool | None = None) -> Self:
1975 """
1976 Find and fix problems with self.face_normals and self.faces
1977 winding direction.
1979 For face normals ensure that vectors are consistently pointed
1980 outwards, and that self.faces is wound in the correct direction
1981 for all connected components.
1983 Parameters
1984 -------------
1985 multibody : None or bool
1986 Fix normals across multiple bodies or if unspecified
1987 check the current `Trimesh.body_count`.
1988 """
1989 if multibody is None:
1990 multibody = self.body_count > 1
1991 repair.fix_normals(self, multibody=multibody)
1992 return self
1994 def fill_holes(self) -> bool:
1995 """
1996 Fill single triangle and single quad holes in the current mesh.
1998 Returns
1999 ----------
2000 watertight : bool
2001 Is the mesh watertight after the function completes
2002 """
2003 return repair.fill_holes(self)
2005 def register(
2006 self, other: Geometry3D | NDArray, **kwargs
2007 ) -> tuple[NDArray[float64], float64]:
2008 """
2009 Align a mesh with another mesh or a PointCloud using
2010 the principal axes of inertia as a starting point which
2011 is refined by iterative closest point.
2013 Parameters
2014 ------------
2015 other : trimesh.Trimesh or (n, 3) float
2016 Mesh or points in space
2017 samples : int
2018 Number of samples from mesh surface to align
2019 icp_first : int
2020 How many ICP iterations for the 9 possible
2021 combinations of
2022 icp_final : int
2023 How many ICP itertations for the closest
2024 candidate from the wider search
2026 Returns
2027 -----------
2028 mesh_to_other : (4, 4) float
2029 Transform to align mesh to the other object
2030 cost : float
2031 Average square distance per point
2032 """
2033 mesh_to_other, cost = registration.mesh_other(mesh=self, other=other, **kwargs)
2034 return mesh_to_other, cost
2036 def compute_stable_poses(
2037 self,
2038 center_mass: NDArray[float64] | None = None,
2039 sigma: Floating = 0.0,
2040 n_samples: Integer = 1,
2041 threshold: Floating = 0.0,
2042 seed: Seed = None,
2043 ) -> tuple[NDArray[float64], NDArray[float64]]:
2044 """
2045 Computes stable orientations of a mesh and their quasi-static probabilities.
2047 This method samples the location of the center of mass from a multivariate
2048 gaussian (mean at com, cov equal to identity times sigma) over n_samples.
2049 For each sample, it computes the stable resting poses of the mesh on a
2050 a planar workspace and evaluates the probabilities of landing in
2051 each pose if the object is dropped onto the table randomly.
2053 This method returns the 4x4 homogeneous transform matrices that place
2054 the shape against the planar surface with the z-axis pointing upwards
2055 and a list of the probabilities for each pose.
2056 The transforms and probabilities that are returned are sorted, with the
2057 most probable pose first.
2059 Parameters
2060 ------------
2061 center_mass : (3, ) float
2062 The object center of mass (if None, this method
2063 assumes uniform density and watertightness and
2064 computes a center of mass explicitly)
2065 sigma : float
2066 The covariance for the multivariate gaussian used
2067 to sample center of mass locations
2068 n_samples : int
2069 The number of samples of the center of mass location
2070 threshold : float
2071 The probability value at which to threshold
2072 returned stable poses
2074 Returns
2075 -------
2076 transforms : (n, 4, 4) float
2077 The homogeneous matrices that transform the
2078 object to rest in a stable pose, with the
2079 new z-axis pointing upwards from the table
2080 and the object just touching the table.
2082 probs : (n, ) float
2083 A probability ranging from 0.0 to 1.0 for each pose
2084 """
2085 return poses.compute_stable_poses(
2086 mesh=self,
2087 center_mass=center_mass,
2088 sigma=sigma,
2089 n_samples=n_samples,
2090 threshold=threshold,
2091 seed=seed,
2092 )
2094 def subdivide(
2095 self, face_index: ArrayLike | None = None, iterations: Integer | None = None
2096 ) -> "Trimesh":
2097 """
2098 Subdivide a mesh with each subdivided face replaced
2099 with four smaller faces. Will return a copy of current
2100 mesh with subdivided faces.
2102 Parameters
2103 ------------
2104 face_index : (m, ) int or None
2105 If None all faces of mesh will be subdivided
2106 If (m, ) int array of indices: only specified faces will be
2107 subdivided. Note that in this case the mesh will generally
2108 no longer be manifold, as the additional vertex on the midpoint
2109 will not be used by the adjacent faces to the faces specified,
2110 and an additional postprocessing step will be required to
2111 make resulting mesh watertight
2112 iterations : int
2113 If passed will run subdivisions multiple times recursively.
2114 NOT COMPATIBLE with `face_index` and will raise a `ValueError`
2115 if both arguments are passed.
2117 Returns
2118 ------------
2119 mesh: trimesh.Trimesh
2120 The copy of current mesh with subdivided faces.
2121 """
2122 if iterations is not None and face_index is not None:
2123 raise ValueError("Unable to subdivide a subset with multiple iterations!")
2125 visual = None
2126 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == (
2127 len(self.vertices),
2128 2,
2129 ):
2130 # uv coords divided along with vertices
2131 vertices, faces, attr = remesh.subdivide(
2132 vertices=np.hstack((self.vertices, self.visual.uv)),
2133 faces=self.faces,
2134 face_index=face_index,
2135 vertex_attributes=self.vertex_attributes,
2136 )
2138 # get a copy of the current visuals
2139 visual = self.visual.copy()
2141 # separate uv coords and vertices
2142 vertices, visual.uv = vertices[:, :3], vertices[:, 3:]
2144 else:
2145 # perform the subdivision with vertex attributes
2146 vertices, faces, attr = remesh.subdivide(
2147 vertices=self.vertices,
2148 faces=self.faces,
2149 face_index=face_index,
2150 vertex_attributes=self.vertex_attributes,
2151 )
2153 # create a new mesh
2154 result = Trimesh(
2155 vertices=vertices,
2156 faces=faces,
2157 visual=visual,
2158 vertex_attributes=attr,
2159 process=False,
2160 )
2162 if iterations is not None and iterations > 1:
2163 return result.subdivide(iterations=iterations - 1)
2165 return result
2167 def subdivide_to_size(
2168 self, max_edge: Number, max_iter: Integer = 10, return_index: bool = False
2169 ) -> "Trimesh | tuple[Trimesh, NDArray[int64]]":
2170 """
2171 Subdivide a mesh until every edge is shorter than a
2172 specified length.
2174 Will return a triangle soup, not a nicely structured mesh.
2176 Parameters
2177 ------------
2178 max_edge
2179 Maximum length of any edge in the result
2180 max_iter : int
2181 The maximum number of times to run subdivision
2182 return_index : bool
2183 If True, return index of original face for new faces
2185 Returns
2186 ------------
2187 mesh: trimesh.Trimesh
2188 The copy of current mesh with subdivided faces.
2189 """
2190 # subdivide vertex attributes
2191 visual = None
2192 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == (
2193 len(self.vertices),
2194 2,
2195 ):
2196 # uv coords divided along with vertices
2197 vertices_faces = remesh.subdivide_to_size(
2198 vertices=np.hstack((self.vertices, self.visual.uv)),
2199 faces=self.faces,
2200 max_edge=max_edge,
2201 max_iter=max_iter,
2202 return_index=return_index,
2203 )
2204 # unpack result
2205 if return_index:
2206 vertices, faces, final_index = vertices_faces
2207 else:
2208 vertices, faces = vertices_faces
2210 # get a copy of the current visuals
2211 visual = self.visual.copy()
2213 # separate uv coords and vertices
2214 vertices, visual.uv = vertices[:, :3], vertices[:, 3:]
2216 else:
2217 # uv coords divided along with vertices
2218 vertices_faces = remesh.subdivide_to_size(
2219 vertices=self.vertices,
2220 faces=self.faces,
2221 max_edge=max_edge,
2222 max_iter=max_iter,
2223 return_index=return_index,
2224 )
2225 # unpack result
2226 if return_index:
2227 vertices, faces, final_index = vertices_faces
2228 else:
2229 vertices, faces = vertices_faces
2231 # create a new mesh
2232 result = Trimesh(vertices=vertices, faces=faces, visual=visual, process=False)
2234 if return_index:
2235 return result, final_index
2237 return result
2239 def subdivide_loop(self, iterations: Integer | None = None) -> "Trimesh":
2240 """
2241 Subdivide a mesh by dividing each triangle into four
2242 triangles and approximating their smoothed surface
2243 using loop subdivision. Loop subdivision often looks
2244 better on triangular meshes than catmul-clark, which
2245 operates primarily on quads.
2247 Parameters
2248 ------------
2249 iterations : int
2250 Number of iterations to run subdivision.
2251 multibody : bool
2252 If True will try to subdivide for each submesh
2254 Returns
2255 ------------
2256 mesh: trimesh.Trimesh
2257 The copy of current mesh with subdivided faces.
2258 """
2259 # perform subdivision for one mesh
2260 new_vertices, new_faces = remesh.subdivide_loop(
2261 vertices=self.vertices, faces=self.faces, iterations=iterations
2262 )
2263 return Trimesh(vertices=new_vertices, faces=new_faces, process=False)
2265 @property
2266 def smooth_shaded(self) -> "Trimesh":
2267 """
2268 Smooth shading in OpenGL relies on which vertices are shared,
2269 this function will disconnect regions above an angle threshold
2270 and return a non-watertight version which will look better
2271 in an OpenGL rendering context.
2273 If you would like to use non-default arguments see `graph.smooth_shade`.
2275 Returns
2276 ---------
2277 smooth_shaded : trimesh.Trimesh
2278 Non watertight version of current mesh.
2279 """
2280 # key this also by the visual properties
2281 # but store it in the mesh cache
2282 self.visual._verify_hash()
2283 cache = self.visual._cache
2284 # needs to be dumped whenever visual or mesh changes
2285 key = f"smooth_shaded_{hash(self.visual)}_{hash(self)}"
2286 if key in cache:
2287 return cache[key]
2288 smooth = graph.smooth_shade(self)
2289 # store it in the mesh cache which dumps when vertices change
2290 cache[key] = smooth
2291 return smooth
2293 @property
2294 def visual(self) -> ColorVisuals | TextureVisuals | None:
2295 """
2296 Get the stored visuals for the current mesh.
2298 Returns
2299 -------------
2300 visual : ColorVisuals or TextureVisuals
2301 Contains visual information about the mesh
2302 """
2303 if hasattr(self, "_visual"):
2304 return self._visual
2305 return None
2307 @visual.setter
2308 def visual(self, value: ColorVisuals | TextureVisuals | None) -> None:
2309 """
2310 When setting a visual object, always make sure
2311 that `visual.mesh` points back to the source mesh.
2313 Parameters
2314 --------------
2315 visual : ColorVisuals or TextureVisuals
2316 Contains visual information about the mesh
2317 """
2318 if value is None:
2319 value = ColorVisuals()
2320 value.mesh = self
2321 self._visual = value
2323 def section(
2324 self, plane_normal: ArrayLike, plane_origin: ArrayLike, **kwargs
2325 ) -> Path3D | None:
2326 """
2327 Returns a 3D cross section of the current mesh and a plane
2328 defined by origin and normal.
2330 Parameters
2331 ------------
2332 plane_normal : (3,) float
2333 Normal vector of section plane.
2334 plane_origin : (3, ) float
2335 Point on the cross section plane.
2337 Returns
2338 ---------
2339 intersections
2340 Curve of intersection or None if it was not hit by plane.
2341 """
2342 # turn line segments into Path2D/Path3D objects
2343 from .path.exchange.misc import lines_to_path
2344 from .path.path import Path3D
2346 # return a single cross section in 3D
2347 lines, _face_index = intersections.mesh_plane(
2348 mesh=self,
2349 plane_normal=plane_normal,
2350 plane_origin=plane_origin,
2351 return_faces=True,
2352 **kwargs,
2353 )
2355 # if the section didn't hit the mesh return None
2356 if len(lines) == 0:
2357 return None
2359 # otherwise load the line segments into the keyword arguments
2360 # for a Path3D object.
2361 path = lines_to_path(lines)
2363 # add the face index info into metadata
2364 # path.metadata["face_index"] = face_index
2366 return Path3D(**path)
2368 def section_multiplane(
2369 self,
2370 plane_origin: ArrayLike,
2371 plane_normal: ArrayLike,
2372 heights: ArrayLike,
2373 ) -> list[Path2D | None]:
2374 """
2375 Return multiple parallel cross sections of the current
2376 mesh in 2D.
2378 Parameters
2379 ------------
2380 plane_origin : (3, ) float
2381 Point on the cross section plane
2382 plane_normal : (3) float
2383 Normal vector of section plane
2384 heights : (n, ) float
2385 Each section is offset by height along
2386 the plane normal.
2388 Returns
2389 ---------
2390 paths : (n, ) Path2D or None
2391 2D cross sections at specified heights.
2392 path.metadata['to_3D'] contains transform
2393 to return 2D section back into 3D space.
2394 """
2395 # turn line segments into Path2D/Path3D objects
2396 from .exchange.load import load_path
2398 # do a multiplane intersection
2399 lines, transforms, faces = intersections.mesh_multiplane(
2400 mesh=self,
2401 plane_normal=plane_normal,
2402 plane_origin=plane_origin,
2403 heights=heights,
2404 )
2406 # turn the line segments into Path2D objects
2407 paths = [None] * len(lines)
2408 for i, f, segments, T in zip(range(len(lines)), faces, lines, transforms):
2409 if len(segments) > 0:
2410 paths[i] = load_path(segments, metadata={"to_3D": T, "face_index": f})
2411 return paths
2413 def slice_plane(
2414 self,
2415 plane_origin: ArrayLike,
2416 plane_normal: ArrayLike,
2417 cap: bool = False,
2418 face_index: ArrayLike | None = None,
2419 **kwargs,
2420 ) -> "Trimesh":
2421 """
2422 Slice the mesh with a plane, returning a new mesh that is the
2423 portion of the original mesh to the positive normal side of the plane
2425 plane_origin : (3,) float
2426 Point on plane to intersect with mesh
2427 plane_normal : (3,) float
2428 Normal vector of plane to intersect with mesh
2429 cap : bool
2430 If True, cap the result with a triangulated polygon
2431 face_index : ((m,) int)
2432 Indexes of mesh.faces to slice. When no mask is
2433 provided, the default is to slice all faces.
2435 Returns
2436 ---------
2437 new_mesh: trimesh.Trimesh or None
2438 Subset of current mesh that intersects the half plane
2439 to the positive normal side of the plane
2440 """
2442 # return a new mesh
2443 new_mesh = intersections.slice_mesh_plane(
2444 mesh=self,
2445 plane_normal=plane_normal,
2446 plane_origin=plane_origin,
2447 cap=cap,
2448 face_index=face_index,
2449 **kwargs,
2450 )
2452 return new_mesh
2454 def unwrap(self, image=None) -> "Trimesh":
2455 """
2456 Returns a Trimesh object equivalent to the current mesh where
2457 the vertices have been assigned uv texture coordinates. Vertices
2458 may be split into as many as necessary by the unwrapping
2459 algorithm, depending on how many uv maps they appear in.
2461 Requires `pip install xatlas`
2463 Parameters
2464 ------------
2465 image : None or PIL.Image
2466 Image to assign to the material
2468 Returns
2469 --------
2470 unwrapped : trimesh.Trimesh
2471 Mesh with unwrapped uv coordinates
2472 """
2473 import xatlas
2475 vmap, faces, uv = xatlas.parametrize(self.vertices, self.faces)
2477 result = Trimesh(
2478 vertices=self.vertices[vmap],
2479 faces=faces,
2480 visual=TextureVisuals(uv=uv, image=image),
2481 process=False,
2482 )
2484 # run additional checks for unwrapping
2485 if tol.strict:
2486 # check the export object to make sure we didn't
2487 # move the indices around on creation
2488 assert np.allclose(result.visual.uv, uv)
2489 assert np.allclose(result.faces, faces)
2490 assert np.allclose(result.vertices, self.vertices[vmap])
2491 # check to make sure indices are still the
2492 # same order after we've exported to OBJ
2493 export = result.export(file_type="obj")
2494 uv_recon = np.array(
2495 [L[3:].split() for L in str.splitlines(export) if L.startswith("vt ")],
2496 dtype=float64,
2497 )
2498 assert np.allclose(uv_recon, uv)
2499 v_recon = np.array(
2500 [L[2:].split() for L in str.splitlines(export) if L.startswith("v ")],
2501 dtype=float64,
2502 )
2503 assert np.allclose(v_recon, self.vertices[vmap])
2505 return result
2507 @cache_decorator
2508 def convex_hull(self) -> "Trimesh":
2509 """
2510 Returns a Trimesh object representing the convex hull of
2511 the current mesh.
2513 Returns
2514 --------
2515 convex : trimesh.Trimesh
2516 Mesh of convex hull of current mesh
2517 """
2518 return convex.convex_hull(self)
2520 def sample(
2521 self,
2522 count: Integer,
2523 return_index: bool = False,
2524 face_weight: NDArray[float64] | None = None,
2525 return_barycentric: bool = False,
2526 seed: Seed = None,
2527 ):
2528 """
2529 Return random samples distributed across the
2530 surface of the mesh
2532 Parameters
2533 ------------
2534 count : int
2535 Number of points to sample
2536 return_index : bool
2537 If True will also return the index of which face each
2538 sample was taken from.
2539 face_weight : None or len(mesh.faces) float
2540 Weight faces by a factor other than face area.
2541 If None will be the same as face_weight=mesh.area
2542 return_barycentric : bool
2543 If True will also return the barycentric coordinates
2544 of each sampled point.
2545 seed : None or int
2546 Seed for deterministic results, otherwise OS entropy.
2548 Returns
2549 ---------
2550 samples : (count, 3) float
2551 Points on surface of mesh
2552 face_index : (count,) int
2553 Index of self.faces
2554 Returned only when return_index is True
2555 barycentric : (count, 3) float
2556 Coordinates on `self.faces[face_index]`
2557 Returned only when return_barycentric is True
2558 """
2559 result = sample.sample_surface(
2560 mesh=self,
2561 count=count,
2562 face_weight=face_weight,
2563 return_barycentric=return_barycentric,
2564 seed=seed,
2565 )
2566 if return_index:
2567 return result
2569 # `sample_surface` always returns the face index we weren't asked for
2570 if return_barycentric:
2571 return result[0], result[2]
2573 return result[0]
2575 def remove_unreferenced_vertices(self) -> None:
2576 """
2577 Remove all vertices in the current mesh which are not
2578 referenced by a face.
2579 """
2580 referenced = np.zeros(len(self.vertices), dtype=bool)
2581 referenced[self.faces] = True
2583 inverse = np.zeros(len(self.vertices), dtype=int64)
2584 inverse[referenced] = np.arange(referenced.sum())
2586 self.update_vertices(mask=referenced, inverse=inverse)
2588 def unmerge_vertices(self) -> None:
2589 """
2590 Removes all face references so that every face contains
2591 three unique vertex indices and no faces are adjacent.
2592 """
2593 # new faces are incrementing so every vertex is unique
2594 faces = np.arange(len(self.faces) * 3, dtype=int64).reshape((-1, 3))
2596 # use update_vertices to apply mask to
2597 # all properties that are per-vertex
2598 self.update_vertices(self.faces.reshape(-1))
2599 # set faces to incrementing indexes
2600 self.faces = faces
2601 # keep face normals as the haven't changed
2602 self._cache.clear(exclude=["face_normals"])
2604 def apply_transform(self, matrix: ArrayLike) -> Self:
2605 """
2606 Transform mesh by a homogeneous transformation matrix.
2608 Does the bookkeeping to avoid recomputing things so this function
2609 should be used rather than directly modifying self.vertices
2610 if possible.
2612 Parameters
2613 ------------
2614 matrix : (4, 4) float
2615 Homogeneous transformation matrix
2616 """
2617 # get c-order float64 matrix
2618 matrix = np.asanyarray(matrix, order="C", dtype=float64)
2620 # only support homogeneous transformations
2621 if matrix.shape != (4, 4):
2622 raise ValueError("Transformation matrix must be (4, 4)!")
2624 # exit early if we've been passed an identity matrix
2625 # np.allclose is surprisingly slow so do this test
2626 elif util.allclose(matrix, _IDENTITY4, 1e-8):
2627 return self
2629 # new vertex positions
2630 new_vertices = transformations.transform_points(self.vertices, matrix=matrix)
2632 # check to see if the matrix has rotation
2633 # rather than just translation
2634 has_rotation = not util.allclose(matrix[:3, :3], _IDENTITY3, atol=1e-6)
2635 # check to see if the matrix flips the winding of triangles
2636 flip = has_rotation and transformations.flips_winding(matrix)
2638 # transform overridden center of mass
2639 if "center_mass" in self._data:
2640 center_mass = [self._data["center_mass"]]
2641 self.center_mass = transformations.transform_points(
2642 center_mass,
2643 matrix,
2644 )[0]
2646 if has_rotation:
2647 linear = matrix[:3, :3]
2648 # the adjugate `det(M) * inv(M)` maps `cross(a, b)` to
2649 # `cross(M @ a, M @ b)` exactly, even for singular `M`
2650 adjugate = np.cross(linear.T[[1, 2, 0]], linear.T[[2, 0, 1]])
2651 if flip:
2652 # faces are reversed below so their normals reverse too
2653 adjugate = -adjugate
2654 if "face_normals" in self._cache:
2655 self._cache["face_normals"] = util.unitize(
2656 np.dot(self.face_normals, adjugate), threshold=0
2657 )
2658 if "vertex_normals" in self._cache:
2659 self._cache["vertex_normals"] = util.unitize(
2660 np.dot(self.vertex_normals, adjugate), threshold=0
2661 )
2663 # if transformation flips winding of triangles
2664 if flip:
2665 log.debug("transform flips winding")
2666 # fliplr will make array non C contiguous
2667 # which will cause hashes to be more
2668 # expensive than necessary so wrap
2669 self.faces = np.ascontiguousarray(np.fliplr(self.faces))
2671 # assign the new values
2672 self.vertices = new_vertices
2674 # preserve normals and topology in cache
2675 # while dumping everything else
2676 self._cache.clear(
2677 exclude={
2678 "face_normals", # transformed by us
2679 "vertex_normals", # also transformed by us
2680 "face_adjacency", # topological
2681 "face_adjacency_edges",
2682 "face_adjacency_unshared",
2683 "edges",
2684 "edges_face",
2685 "edges_sorted",
2686 "edges_unique",
2687 "edges_unique_idx",
2688 "edges_unique_inverse",
2689 "edges_sparse",
2690 "body_count",
2691 "faces_unique_edges",
2692 "euler_number",
2693 }
2694 )
2695 # set the cache ID with the current hash value
2696 self._cache.id_set()
2697 return self
2699 def voxelized(self, pitch: Floating | None, method: str = "subdivide", **kwargs):
2700 """
2701 Return a VoxelGrid object representing the current mesh
2702 discretized into voxels at the specified pitch
2704 Parameters
2705 ------------
2706 pitch : float
2707 The edge length of a single voxel
2708 method: implementation key. See `trimesh.voxel.creation.voxelizers`
2709 **kwargs: additional kwargs passed to the specified implementation.
2711 Returns
2712 ----------
2713 voxelized : VoxelGrid object
2714 Representing the current mesh
2715 """
2716 from .voxel import creation
2718 return creation.voxelize(mesh=self, pitch=pitch, method=method, **kwargs)
2720 def simplify_quadric_decimation(
2721 self,
2722 percent: Floating | None = None,
2723 face_count: Integer | None = None,
2724 aggression: Integer | None = None,
2725 ) -> "Trimesh":
2726 """
2727 A thin wrapper around `pip install fast-simplification`.
2729 Parameters
2730 -----------
2731 percent
2732 A number between 0.0 and 1.0 for how much of the mesh to
2733 remove, i.e. `percent=0.9` keeps roughly 10% of the faces.
2734 Passed to `fast_simplification` as `target_reduction`.
2735 face_count
2736 Target number of faces desired in the resulting mesh.
2737 Pass either `percent` or `face_count`, not both.
2738 aggression
2739 An integer between `0` and `10`, the scale being roughly
2740 `0` is "slow and good" and `10` being "fast and bad."
2742 Returns
2743 ---------
2744 simple : trimesh.Trimesh
2745 Simplified version of mesh.
2746 """
2747 from fast_simplification import simplify
2749 # create keyword arguments as dict so we can filter out `None`
2750 # values as the C wrapper as of writing is not happy with `None`
2751 # and requires they be omitted from the constructor
2752 kwargs = {
2753 "target_count": face_count,
2754 "target_reduction": percent,
2755 "agg": aggression,
2756 }
2758 # todo : one could take the `return_collapses=True` array and use it to
2759 # apply the same simplification to the visual info
2760 vertices, faces = simplify(
2761 points=self.vertices.view(np.ndarray),
2762 triangles=self.faces.view(np.ndarray),
2763 **{k: v for k, v in kwargs.items() if v is not None},
2764 )
2766 return Trimesh(vertices=vertices, faces=faces)
2768 def outline(self, face_ids: NDArray[int64] | None = None, **kwargs) -> Path3D:
2769 """
2770 Given a list of face indexes find the outline of those
2771 faces and return it as a Path3D.
2773 The outline is defined here as every edge which is only
2774 included by a single triangle.
2776 Note that this implies a non-watertight mesh as the
2777 outline of a watertight mesh is an empty path.
2779 Parameters
2780 ------------
2781 face_ids : (n, ) int
2782 Indices to compute the outline of.
2783 If None, outline of full mesh will be computed.
2784 **kwargs: passed to Path3D constructor
2786 Returns
2787 ----------
2788 path : Path3D
2789 Curve in 3D of the outline
2790 """
2791 from .path.exchange.misc import faces_to_path
2793 return Path3D(**faces_to_path(self, face_ids, **kwargs))
2795 def projected(self, normal: ArrayLike, **kwargs) -> Path2D:
2796 """
2797 Project a mesh onto a plane and then extract the
2798 polygon that outlines the mesh projection on that
2799 plane.
2801 Parameters
2802 ----------
2803 normal : (3,) float
2804 Normal to extract flat pattern along
2805 origin : None or (3,) float
2806 Origin of plane to project mesh onto
2807 ignore_sign : bool
2808 Allow a projection from the normal vector in
2809 either direction: this provides a substantial speedup
2810 on watertight meshes where the direction is irrelevant
2811 but if you have a triangle soup and want to discard
2812 backfaces you should set this to False.
2813 rpad : float
2814 Proportion to pad polygons by before unioning
2815 and then de-padding result by to avoid zero-width gaps.
2816 apad : float
2817 Absolute padding to pad polygons by before unioning
2818 and then de-padding result by to avoid zero-width gaps.
2819 tol_dot : float
2820 Tolerance for discarding on-edge triangles.
2821 precise : bool
2822 Use the precise projection computation using shapely.
2823 precise_eps : float
2824 Tolerance for precise triangle checks.
2826 Returns
2827 ----------
2828 projected : trimesh.path.Path2D
2829 Outline of source mesh
2830 """
2831 from .exchange.load import load_path
2832 from .path import Path2D
2833 from .path.polygons import projected
2835 projection = projected(mesh=self, normal=normal, **kwargs)
2836 if projection is None:
2837 return Path2D()
2838 return load_path(projection)
2840 @cache_decorator
2841 def area(self) -> float64:
2842 """
2843 Summed area of all triangles in the current mesh.
2845 Returns
2846 ---------
2847 area : float
2848 Surface area of mesh
2849 """
2850 area = self.area_faces.sum()
2851 return area
2853 @cache_decorator
2854 def area_faces(self) -> NDArray[float64]:
2855 """
2856 The area of each face in the mesh.
2858 Returns
2859 ---------
2860 area_faces : (n, ) float
2861 Area of each face
2862 """
2863 return triangles.area(crosses=self.triangles_cross)
2865 @cache_decorator
2866 def mass_properties(self) -> MassProperties:
2867 """
2868 Returns the mass properties of the current mesh.
2870 Assumes uniform density, and result is probably garbage if mesh
2871 isn't watertight.
2873 Returns
2874 ----------
2875 properties : dict
2876 With keys:
2877 'volume' : in global units^3
2878 'mass' : From specified density
2879 'density' : Included again for convenience (same as kwarg density)
2880 'inertia' : Taken at the center of mass and aligned with global
2881 coordinate system
2882 'center_mass' : Center of mass location, in global coordinate system
2883 """
2884 # if the density or center of mass was overridden they will be put into data
2885 density = self._data.data.get("density", None)
2886 center_mass = self._data.data.get("center_mass", None)
2887 return triangles.mass_properties(
2888 triangles=self.triangles,
2889 crosses=self.triangles_cross,
2890 density=density,
2891 center_mass=center_mass,
2892 skip_inertia=False,
2893 )
2895 def invert(self) -> Self:
2896 """
2897 Invert the mesh in-place by reversing the winding of every
2898 face and negating normals without dumping the cache.
2900 Alters `self.faces` by reversing columns, and negating
2901 `self.face_normals` and `self.vertex_normals`.
2902 """
2903 with self._cache:
2904 if "face_normals" in self._cache:
2905 self.face_normals = self._cache["face_normals"] * -1.0
2906 if "vertex_normals" in self._cache:
2907 self.vertex_normals = self._cache["vertex_normals"] * -1.0
2908 # fliplr makes array non-contiguous so cache checks slow
2909 self.faces = np.ascontiguousarray(np.fliplr(self.faces))
2910 # save our normals
2911 self._cache.clear(exclude=["face_normals", "vertex_normals"])
2913 return self
2915 def scene(self, **kwargs) -> Scene:
2916 """
2917 Returns a Scene object containing the current mesh.
2919 Returns
2920 ---------
2921 scene : trimesh.scene.scene.Scene
2922 Contains just the current mesh
2923 """
2924 return Scene(self, **kwargs)
2926 def show(
2927 self,
2928 viewer: ViewerType = None,
2929 **kwargs,
2930 ) -> Scene:
2931 """
2932 Render the mesh in an opengl window. Requires pyglet.
2934 Parameters
2935 ------------
2936 viewer : ViewerType
2937 What kind of viewer to use, such as
2938 `gl` to open a pyglet window
2939 `jupyter` for a jupyter notebook
2940 `marimo'` for a marimo notebook
2941 None for a "best guess"
2942 smooth : bool
2943 Run smooth shading on mesh or not,
2944 large meshes will be slow
2946 Returns
2947 -----------
2948 scene : trimesh.scene.Scene
2949 Scene with current mesh in it
2950 """
2951 scene = self.scene()
2952 return scene.show(viewer=viewer, **kwargs)
2954 def submesh(
2955 self,
2956 faces_sequence: Sequence[ArrayLike],
2957 only_watertight: bool = False,
2958 repair: bool = False,
2959 **kwargs,
2960 ) -> "Trimesh | list[Trimesh]":
2961 """
2962 Return a subset of the mesh.
2964 Parameters
2965 ------------
2966 faces_sequence : sequence (m, ) int
2967 Face indices of mesh
2968 only_watertight : bool
2969 Only return submeshes which are watertight
2970 repair
2971 Try to repair the submesh if it is not watertight
2972 append : bool
2973 Return a single mesh which has the faces appended.
2974 if this flag is set, only_watertight is ignored
2976 Returns
2977 ---------
2978 submesh : Trimesh or (n,) Trimesh
2979 Single mesh if `append` or list of submeshes
2980 """
2981 return util.submesh(
2982 mesh=self,
2983 faces_sequence=faces_sequence,
2984 only_watertight=only_watertight,
2985 repair=repair,
2986 **kwargs,
2987 )
2989 @cache_decorator
2990 def identifier(self) -> NDArray[float64]:
2991 """
2992 Return a float vector which is unique to the mesh
2993 and is robust to rotation and translation.
2995 Returns
2996 -----------
2997 identifier : (7,) float
2998 Identifying properties of the current mesh
2999 """
3000 return comparison.identifier_simple(self)
3002 @cache_decorator
3003 def identifier_hash(self) -> str:
3004 """
3005 A hash of the rotation invariant identifier vector.
3007 Returns
3008 ---------
3009 hashed : str
3010 Hex string of the SHA256 hash from
3011 the identifier vector at hand-tuned sigfigs.
3012 """
3013 return comparison.identifier_hash(self.identifier)
3015 def export(
3016 self,
3017 file_obj: Loadable = None,
3018 file_type: str | None = None,
3019 **kwargs,
3020 ) -> dict | bytes | str:
3021 """
3022 Export the current mesh to a file object.
3023 If file_obj is a filename, file will be written there.
3025 Supported formats are stl, off, ply, collada, json,
3026 dict, glb, dict64, msgpack.
3028 Parameters
3029 ------------
3030 file_obj : open writeable file object
3031 str, file name where to save the mesh
3032 None, return the export blob
3033 file_type : str
3034 Which file type to export as, if `file_name`
3035 is passed this is not required.
3037 Returns
3038 ----------
3039 exported : bytes or str
3040 Result of exporter
3041 """
3042 return export_mesh(mesh=self, file_obj=file_obj, file_type=file_type, **kwargs)
3044 def to_dict(self) -> dict[str, str | list[list[float]] | list[list[int]]]:
3045 """
3046 Return a dictionary representation of the current mesh
3047 with keys that can be used as the kwargs for the
3048 Trimesh constructor and matches the schema in:
3049 `trimesh/resources/schema/primitive/trimesh.schema.json`
3051 Returns
3052 ----------
3053 result : dict
3054 Matches schema and Trimesh constructor.
3055 """
3056 return {
3057 "kind": "trimesh",
3058 "vertices": self.vertices.tolist(),
3059 "faces": self.faces.tolist(),
3060 }
3062 def convex_decomposition(self, **kwargs) -> list["Trimesh"]:
3063 """
3064 Compute an approximate convex decomposition of a mesh
3065 using `pip install pyVHACD`.
3067 Returns
3068 -------
3069 meshes
3070 List of convex meshes that approximate the original
3071 **kwargs : VHACD keyword arguments
3072 """
3073 return [
3074 Trimesh(**kwargs)
3075 for kwargs in decomposition.convex_decomposition(self, **kwargs)
3076 ]
3078 def union(
3079 self,
3080 other: "Trimesh | Sequence[Trimesh]",
3081 engine: BooleanEngineType = None,
3082 check_volume: bool = True,
3083 **kwargs,
3084 ) -> "Trimesh":
3085 """
3086 Boolean union between this mesh and other meshes.
3088 Parameters
3089 ------------
3090 other : Trimesh or (n, ) Trimesh
3091 Other meshes to union
3092 engine
3093 Which backend to use, the default
3094 recommendation is: `pip install manifold3d`.
3095 check_volume
3096 Raise an error if not all meshes are watertight
3097 positive volumes. Advanced users may want to ignore
3098 this check as it is expensive.
3099 kwargs
3100 Passed through to the `engine`.
3102 Returns
3103 ---------
3104 union : trimesh.Trimesh
3105 Union of self and other Trimesh objects
3106 """
3107 return boolean.union(
3108 meshes=util.chain(self, other),
3109 engine=engine,
3110 check_volume=check_volume,
3111 **kwargs,
3112 )
3114 def difference(
3115 self,
3116 other: "Trimesh | Sequence[Trimesh]",
3117 engine: BooleanEngineType = None,
3118 check_volume: bool = True,
3119 **kwargs,
3120 ) -> "Trimesh":
3121 """
3122 Boolean difference between this mesh and other meshes.
3124 Parameters
3125 ------------
3126 other
3127 One or more meshes to difference with the current mesh.
3128 engine
3129 Which backend to use, the default
3130 recommendation is: `pip install manifold3d`.
3131 check_volume
3132 Raise an error if not all meshes are watertight
3133 positive volumes. Advanced users may want to ignore
3134 this check as it is expensive.
3135 kwargs
3136 Passed through to the `engine`.
3138 Returns
3139 ---------
3140 difference : trimesh.Trimesh
3141 Difference between self and other Trimesh objects
3142 """
3143 return boolean.difference(
3144 meshes=util.chain(self, other),
3145 engine=engine,
3146 check_volume=check_volume,
3147 **kwargs,
3148 )
3150 def intersection(
3151 self,
3152 other: "Trimesh | Sequence[Trimesh]",
3153 engine: BooleanEngineType = None,
3154 check_volume: bool = True,
3155 **kwargs,
3156 ) -> "Trimesh":
3157 """
3158 Boolean intersection between this mesh and other meshes.
3160 Parameters
3161 ------------
3162 other : trimesh.Trimesh, or list of trimesh.Trimesh objects
3163 Meshes to calculate intersections with
3164 engine
3165 Which backend to use, the default
3166 recommendation is: `pip install manifold3d`.
3167 check_volume
3168 Raise an error if not all meshes are watertight
3169 positive volumes. Advanced users may want to ignore
3170 this check as it is expensive.
3171 kwargs
3172 Passed through to the `engine`.
3174 Returns
3175 ---------
3176 intersection : trimesh.Trimesh
3177 Mesh of the volume contained by all passed meshes
3178 """
3179 return boolean.intersection(
3180 meshes=util.chain(self, other),
3181 engine=engine,
3182 check_volume=check_volume,
3183 **kwargs,
3184 )
3186 def contains(self, points: ArrayLike) -> NDArray[np.bool_]:
3187 """
3188 Given an array of points determine whether or not they
3189 are inside the mesh. This raises an error if called on a
3190 non-watertight mesh.
3192 Parameters
3193 ------------
3194 points : (n, 3) float
3195 Points in cartesian space
3197 Returns
3198 ---------
3199 contains : (n, ) bool
3200 Whether or not each point is inside the mesh
3201 """
3202 return self.ray.contains_points(points)
3204 @cache_decorator
3205 def face_angles(self) -> NDArray[float64]:
3206 """
3207 Returns the angle at each vertex of a face.
3209 Returns
3210 --------
3211 angles : (len(self.faces), 3) float
3212 Angle at each vertex of a face
3213 """
3214 return triangles.angles(self.triangles)
3216 @cache_decorator
3217 def face_angles_sparse(self) -> coo_matrix:
3218 """
3219 A sparse matrix representation of the face angles.
3221 Returns
3222 ----------
3223 sparse : scipy.sparse.coo_matrix
3224 Float sparse matrix with with shape:
3225 (len(self.vertices), len(self.faces))
3226 """
3227 angles = curvature.face_angles_sparse(self)
3228 return angles
3230 @cache_decorator
3231 def vertex_defects(self) -> NDArray[float64]:
3232 """
3233 Return the vertex defects, or (2*pi) minus the sum of the angles
3234 of every face that includes that vertex.
3236 If a vertex is only included by coplanar triangles, this
3237 will be zero. For convex regions this is positive, and
3238 concave negative.
3240 Returns
3241 --------
3242 vertex_defect : (len(self.vertices), ) float
3243 Vertex defect at the every vertex
3244 """
3245 defects = curvature.vertex_defects(self)
3246 return defects
3248 @cache_decorator
3249 def vertex_degree(self) -> NDArray[int64]:
3250 """
3251 Return the number of faces each vertex is included in.
3253 Returns
3254 ----------
3255 degree : (len(self.vertices), ) int
3256 Number of faces each vertex is included in
3257 """
3258 # get degree through sparse matrix
3259 degree = np.array(self.faces_sparse.sum(axis=1)).flatten()
3260 return degree
3262 @cache_decorator
3263 def face_adjacency_tree(self) -> Index:
3264 """
3265 An R-tree of face adjacencies.
3267 Returns
3268 --------
3269 tree
3270 Where each edge in self.face_adjacency has a
3271 rectangular cell
3272 """
3273 # the (n,6) interleaved bounding box for every line segment
3274 return util.bounds_tree(
3275 np.column_stack(
3276 (
3277 self.vertices[self.face_adjacency_edges].min(axis=1),
3278 self.vertices[self.face_adjacency_edges].max(axis=1),
3279 )
3280 )
3281 )
3283 def copy(self, include_cache: bool = False, include_visual: bool = True) -> "Trimesh":
3284 """
3285 Safely return a copy of the current mesh.
3287 By default, copied meshes will have emptied cache
3288 to avoid memory issues and so may be slow on initial
3289 operations until caches are regenerated.
3291 Current object will *never* have its cache cleared.
3293 Parameters
3294 ------------
3295 include_cache : bool
3296 If True, will shallow copy cached data to new mesh
3297 include_visual : bool
3298 If True, will copy visual information
3300 Returns
3301 ---------
3302 copied : trimesh.Trimesh
3303 Copy of current mesh
3304 """
3305 # start with an empty mesh
3306 copied = Trimesh()
3307 # always deepcopy vertex and face data
3308 copied._data.data = deepcopy(self._data.data)
3310 if include_visual:
3311 # copy visual information
3312 copied.visual = self.visual.copy()
3314 copied.vertex_attributes.update(
3315 {k: deepcopy(v) for k, v in self.vertex_attributes.items()}
3316 )
3317 copied.face_attributes.update(
3318 {k: deepcopy(v) for k, v in self.face_attributes.items()}
3319 )
3321 # get metadata
3322 copied.metadata = deepcopy(self.metadata)
3324 # make sure cache ID is set initially
3325 copied._cache.verify()
3327 if include_cache:
3328 # shallow copy cached items into the new cache
3329 # since the data didn't change here when the
3330 # data in the new mesh is changed these items
3331 # will be dumped in the new mesh but preserved
3332 # in the original mesh
3333 copied._cache.cache.update(self._cache.cache)
3335 return copied
3337 def __deepcopy__(self, *args) -> "Trimesh":
3338 # interpret deep copy as "get rid of cached data"
3339 return self.copy(include_cache=False)
3341 def __copy__(self, *args) -> "Trimesh":
3342 # interpret shallow copy as "keep cached data"
3343 return self.copy(include_cache=True)
3345 def eval_cached(self, statement: str, *args) -> Any:
3346 """
3347 DEPRECATED: call `eval` directly instead.
3349 Evaluate a statement and cache the result before returning.
3351 Parameters
3352 ------------
3353 statement : str
3354 Statement of valid python code
3355 *args : list
3356 Available inside statement as args[0], etc
3358 Returns
3359 -----------
3360 result : result of running eval on statement with args
3362 Examples
3363 -----------
3364 r = mesh.eval_cached('np.dot(self.vertices, args[0])', [0, 0, 1])
3365 """
3366 import warnings
3368 warnings.warn(
3369 "`Trimesh.eval_cached` is deprecated "
3370 + "and will be removed in a future release. "
3371 + "call `eval` directly if you need this behavior.",
3372 category=DeprecationWarning,
3373 stacklevel=2,
3374 )
3376 # store this by the combined hash of statement and args
3377 hashable = [hash(statement)]
3378 hashable.extend(hash(a) for a in args)
3380 key = f"eval_cached_{hash(tuple(hashable))}"
3382 if key in self._cache:
3383 return self._cache[key]
3385 result = eval(statement)
3386 self._cache[key] = result
3387 return result
3389 def __add__(self, other: "Trimesh") -> "Trimesh":
3390 """
3391 Concatenate the mesh with another mesh.
3393 Parameters
3394 ------------
3395 other : trimesh.Trimesh object
3396 Mesh to be concatenated with self
3398 Returns
3399 ----------
3400 concat : trimesh.Trimesh
3401 Mesh object of combined result
3402 """
3403 concat = util.concatenate(self, other)
3404 return concat