Coverage for trimesh/base.py: 93%

815 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2# trimesh 

3 

4https://github.com/mikedh/trimesh 

5--------------------------------- 

6 

7Library for importing, exporting and doing simple operations on triangular meshes. 

8""" 

9 

10from copy import deepcopy 

11from typing import Any 

12 

13import numpy as np 

14from numpy import float64, int64, ndarray 

15 

16from . import ( 

17 boolean, 

18 comparison, 

19 convex, 

20 curvature, 

21 decomposition, 

22 geometry, 

23 graph, 

24 grouping, 

25 inertia, 

26 intersections, 

27 permutate, 

28 poses, 

29 proximity, 

30 ray, 

31 registration, 

32 remesh, 

33 repair, 

34 sample, 

35 transformations, 

36 triangles, 

37 units, 

38 util, 

39 visual, 

40) 

41from .caching import Cache, DataStore, TrackedArray, cache_decorator 

42from .constants import log, tol 

43from .exceptions import ExceptionWrapper 

44from .exchange.export import export_mesh 

45from .parent import Geometry3D 

46from .scene import Scene 

47from .triangles import MassProperties 

48from .typed import ( 

49 ArrayLike, 

50 BooleanEngineType, 

51 Floating, 

52 Integer, 

53 Loadable, 

54 NDArray, 

55 Number, 

56 Self, 

57 Sequence, 

58 ViewerType, 

59) 

60from .visual import ColorVisuals, TextureVisuals, create_visual 

61 

62try: 

63 from scipy.sparse import coo_matrix 

64 from scipy.spatial import cKDTree 

65except BaseException as E: 

66 cKDTree = ExceptionWrapper(E) 

67 coo_matrix = ExceptionWrapper(E) 

68try: 

69 from networkx import Graph 

70except BaseException as E: 

71 Graph = ExceptionWrapper(E) 

72 

73try: 

74 from PIL import Image 

75except BaseException as E: 

76 Image = ExceptionWrapper(E) 

77 

78try: 

79 from rtree.index import Index 

80except BaseException as E: 

81 Index = ExceptionWrapper(E) 

82 

83try: 

84 from .path import Path2D, Path3D 

85except BaseException as E: 

86 Path2D = ExceptionWrapper(E) 

87 Path3D = ExceptionWrapper(E) 

88 

89# save immutable identity matrices for checks 

90_IDENTITY3 = np.eye(3, dtype=np.float64) 

91_IDENTITY3.flags.writeable = False 

92_IDENTITY4 = np.eye(4, dtype=np.float64) 

93_IDENTITY4.flags.writeable = False 

94 

95 

96class Trimesh(Geometry3D): 

97 def __init__( 

98 self, 

99 vertices: ArrayLike | None = None, 

100 faces: ArrayLike | None = None, 

101 face_normals: ArrayLike | None = None, 

102 vertex_normals: ArrayLike | None = None, 

103 face_colors: ArrayLike | None = None, 

104 vertex_colors: ArrayLike | None = None, 

105 face_attributes: dict[str, ArrayLike] | None = None, 

106 vertex_attributes: dict[str, ArrayLike] | None = None, 

107 metadata: dict[str, Any] | None = None, 

108 process: bool = True, 

109 validate: bool = False, 

110 merge_tex: bool | None = None, 

111 merge_norm: bool | None = None, 

112 use_embree: bool = True, 

113 initial_cache: dict[str, ndarray] | None = None, 

114 visual: ColorVisuals | TextureVisuals | None = None, 

115 **kwargs, 

116 ) -> None: 

117 """ 

118 A Trimesh object contains a triangular 3D mesh. 

119 

120 Parameters 

121 ------------ 

122 vertices : (n, 3) float 

123 Array of vertex locations 

124 faces : (m, 3) or (m, 4) int 

125 Array of triangular or quad faces (triangulated on load) 

126 face_normals : (m, 3) float 

127 Array of normal vectors corresponding to faces 

128 vertex_normals : (n, 3) float 

129 Array of normal vectors for vertices 

130 face_colors : (n, 3|4) uint8 

131 Array of colors for faces 

132 vertex_colors : (n, 3|4) uint8 

133 Array of colors for vertices 

134 face_attributes : dict 

135 Attributes corresponding to faces 

136 vertex_attributes : dict 

137 Attributes corresponding to vertices 

138 metadata : dict 

139 Any metadata about the mesh 

140 process : bool 

141 if True, Nan and Inf values will be removed 

142 immediately and vertices will be merged 

143 validate : bool 

144 If True, degenerate and duplicate faces will be 

145 removed immediately, and some functions will alter 

146 the mesh to ensure consistent results. 

147 merge_tex : bool 

148 If True textured meshes with UV coordinates will 

149 have vertices merged regardless of UV coordinates 

150 merge_norm : bool 

151 If True, meshes with vertex normals will have 

152 vertices merged ignoring different normals 

153 use_embree : bool 

154 If True try to use pyembree raytracer. 

155 If pyembree is not available it will automatically fall 

156 back to a much slower rtree/numpy implementation 

157 initial_cache : dict 

158 A way to pass things to the cache in case expensive 

159 things were calculated before creating the mesh object. 

160 visual : ColorVisuals or TextureVisuals 

161 Assigned to self.visual 

162 """ 

163 

164 # self._data stores information about the mesh which 

165 # CANNOT be regenerated. 

166 # in the base class all that is stored here is vertex and 

167 # face information 

168 # any data put into the store is converted to a TrackedArray 

169 # which is a subclass of np.ndarray that provides hash and crc 

170 # methods which can be used to detect changes in the array. 

171 self._data = DataStore() 

172 

173 # self._cache stores information about the mesh which CAN be 

174 # regenerated from self._data, but may be slow to calculate. 

175 # In order to maintain consistency 

176 # the cache is cleared when self._data.__hash__() changes 

177 self._cache = Cache(id_function=self._data.__hash__, force_immutable=True) 

178 if initial_cache is not None: 

179 self._cache.update(initial_cache) 

180 

181 # check for None only to avoid warning messages in subclasses 

182 

183 # (n, 3) float array of vertices 

184 self.vertices = vertices 

185 

186 # (m, 3) int of triangle faces that references self.vertices 

187 self.faces = faces 

188 

189 # store per-face and per-vertex attributes which will 

190 # be updated when an update_faces call is made 

191 self.face_attributes = {} 

192 self.vertex_attributes = {} 

193 

194 # hold visual information about the mesh (vertex and face colors) 

195 if visual is None: 

196 self.visual = create_visual( 

197 face_colors=face_colors, vertex_colors=vertex_colors, mesh=self 

198 ) 

199 else: 

200 self.visual = visual 

201 

202 # if we've been passed a visual object 

203 if vertex_colors is not None: 

204 self.vertex_attributes["color"] = vertex_colors 

205 if face_colors is not None: 

206 self.face_attributes["color"] = face_colors 

207 

208 # normals are accessed through setters/properties and are regenerated 

209 # if dimensions are inconsistent, but can be set by the constructor 

210 # to avoid a substantial number of cross products 

211 if face_normals is not None: 

212 self.face_normals = face_normals 

213 

214 # (n, 3) float of vertex normals, can be created from face normals 

215 if vertex_normals is not None: 

216 self.vertex_normals = vertex_normals 

217 

218 # embree is a much, much faster raytracer written by Intel 

219 # if you have pyembree installed you should use it 

220 # although both raytracers were designed to have a common API 

221 if ray.has_embree and use_embree: 

222 self.ray = ray.ray_pyembree.RayMeshIntersector(self) 

223 else: 

224 # create a ray-mesh query object for the current mesh 

225 # initializing is very inexpensive and object is convenient to have. 

226 # On first query expensive bookkeeping is done (creation of r-tree), 

227 # and is cached for subsequent queries 

228 self.ray = ray.ray_triangle.RayMeshIntersector(self) 

229 

230 # a quick way to get permuted versions of the current mesh 

231 self.permutate = permutate.Permutator(self) 

232 

233 # convenience class for nearest point queries 

234 self.nearest = proximity.ProximityQuery(self) 

235 

236 # update the mesh metadata with passed metadata 

237 self.metadata = {} 

238 if isinstance(metadata, dict): 

239 self.metadata.update(metadata) 

240 elif metadata is not None: 

241 raise ValueError(f"metadata should be a dict or None, got {metadata!s}") 

242 

243 # use update to copy items 

244 if face_attributes is not None: 

245 self.face_attributes.update(face_attributes) 

246 if vertex_attributes is not None: 

247 self.vertex_attributes.update(vertex_attributes) 

248 

249 # process will remove NaN and Inf values and merge vertices 

250 # if validate, will remove degenerate and duplicate faces 

251 if process or validate: 

252 self.process(validate=validate, merge_tex=merge_tex, merge_norm=merge_norm) 

253 

254 def process( 

255 self, 

256 validate: bool = False, 

257 merge_tex: bool | None = None, 

258 merge_norm: bool | None = None, 

259 ) -> Self: 

260 """ 

261 Do processing to make a mesh useful. 

262 

263 Does this by: 

264 1) removing NaN and Inf values 

265 2) merging duplicate vertices 

266 If validate: 

267 3) Remove triangles which have one edge 

268 of their 2D oriented bounding box 

269 shorter than tol.merge 

270 4) remove duplicated triangles 

271 5) Attempt to ensure triangles are consistently wound 

272 and normals face outwards. 

273 

274 Parameters 

275 ------------ 

276 validate : bool 

277 Remove degenerate and duplicate faces. 

278 merge_tex : bool 

279 If True textured meshes with UV coordinates will 

280 have vertices merged regardless of UV coordinates 

281 merge_norm : bool 

282 If True, meshes with vertex normals will have 

283 vertices merged ignoring different normals 

284 

285 Returns 

286 ------------ 

287 self: trimesh.Trimesh 

288 Current mesh 

289 """ 

290 # if there are no vertices or faces exit early 

291 if self.is_empty: 

292 return self 

293 

294 # if we're cleaning remove duplicate and degenerate faces. this 

295 # mutates face count so it must run OUTSIDE the cache lock — locking 

296 # across a face-count change leaves derived caches (face_adjacency, 

297 # edges, ...) stale, which fix_normals would then read and blow up on. 

298 if validate: 

299 # get a mask with only unique and non-degenerate faces 

300 mask = self.unique_faces() & self.nondegenerate_faces() 

301 self.update_faces(mask) 

302 self.fix_normals() 

303 

304 # the remaining ops do not change face/vertex count so we can hold 

305 # the cache lock to preserve face_normals/vertex_normals across them 

306 with self._cache: 

307 self.remove_infinite_values() 

308 self.merge_vertices(merge_tex=merge_tex, merge_norm=merge_norm) 

309 self._cache.clear(exclude={"face_normals", "vertex_normals"}) 

310 

311 self.metadata["processed"] = True 

312 return self 

313 

314 @property 

315 def mutable(self) -> bool: 

316 """ 

317 Is the current mesh allowed to be altered in-place? 

318 

319 Returns 

320 ------------- 

321 mutable 

322 If data is allowed to be set for the mesh. 

323 """ 

324 return self._data.mutable 

325 

326 @mutable.setter 

327 def mutable(self, value: bool) -> None: 

328 """ 

329 Set the mutability of the current mesh. 

330 

331 Parameters 

332 ---------- 

333 value 

334 Change whether the current mesh is allowed to be altered in-place. 

335 """ 

336 self._data.mutable = value 

337 

338 @property 

339 def faces(self) -> TrackedArray: 

340 """ 

341 The faces of the mesh. 

342 

343 This is regarded as core information which cannot be 

344 regenerated from cache and as such is stored in 

345 `self._data` which tracks the array for changes and 

346 clears cached values of the mesh altered. 

347 

348 Returns 

349 ---------- 

350 faces : (n, 3) int64 

351 References for `self.vertices` for triangles. 

352 """ 

353 return self._data["faces"] 

354 

355 @faces.setter 

356 def faces(self, values: ArrayLike | None) -> None: 

357 """ 

358 Set the vertex indexes that make up triangular faces. 

359 

360 Parameters 

361 -------------- 

362 values : (n, 3) int64 

363 Indexes of self.vertices 

364 """ 

365 if values is None: 

366 # if passed none store an empty array 

367 values = np.zeros(shape=(0, 3), dtype=int64) 

368 else: 

369 values = np.asanyarray(values, dtype=int64) 

370 

371 # automatically triangulate quad faces 

372 if len(values.shape) == 2 and values.shape[1] != 3: 

373 log.info("triangulating faces") 

374 values = geometry.triangulate_quads(values) 

375 

376 self._data["faces"] = values 

377 

378 @cache_decorator 

379 def faces_sparse(self) -> coo_matrix: 

380 """ 

381 A sparse matrix representation of the faces. 

382 

383 Returns 

384 ---------- 

385 sparse : scipy.sparse.coo_matrix 

386 Has properties: 

387 dtype : bool 

388 shape : (len(self.vertices), len(self.faces)) 

389 """ 

390 return geometry.index_sparse(columns=len(self.vertices), indices=self.faces) 

391 

392 @property 

393 def face_normals(self) -> NDArray[float64]: 

394 """ 

395 Return the unit normal vector for each face. 

396 

397 If a face is degenerate and a normal can't be generated 

398 a zero magnitude unit vector will be returned for that face. 

399 

400 Returns 

401 ----------- 

402 normals : (len(self.faces), 3) float64 

403 Normal vectors of each face 

404 """ 

405 # check shape of cached normals 

406 cached = self._cache["face_normals"] 

407 # get faces from datastore 

408 if "faces" in self._data: 

409 faces = self._data.data["faces"] 

410 else: 

411 faces = None 

412 

413 # if we have no faces exit early 

414 if faces is None or len(faces) == 0: 

415 return np.array([], dtype=float64).reshape((0, 3)) 

416 

417 # if the shape of cached normals equals the shape of faces return 

418 if np.shape(cached) == np.shape(faces): 

419 return cached 

420 

421 # use cached triangle cross products to generate normals 

422 # this will always return the correct shape but some values 

423 # will be zero or an arbitrary vector if the inputs had 

424 # a cross product below machine epsilon 

425 normals, valid = triangles.normals( 

426 triangles=self.triangles, crosses=self.triangles_cross 

427 ) 

428 

429 # if all triangles are valid shape is correct 

430 if valid.all(): 

431 # put calculated face normals into cache manually 

432 self._cache["face_normals"] = normals 

433 return normals 

434 

435 # make a padded list of normals for correct shape 

436 padded = np.zeros((len(self.triangles), 3), dtype=float64) 

437 padded[valid] = normals 

438 

439 # put calculated face normals into cache manually 

440 self._cache["face_normals"] = padded 

441 

442 return padded 

443 

444 @face_normals.setter 

445 def face_normals(self, values: ArrayLike | None) -> None: 

446 """ 

447 Assign values to face normals. 

448 

449 Parameters 

450 ------------- 

451 values : (len(self.faces), 3) float 

452 Unit face normals. If None will clear existing normals. 

453 """ 

454 # if nothing passed exit 

455 if values is None: 

456 return 

457 # make sure candidate face normals are C-contiguous float 

458 values = np.asanyarray(values, order="C", dtype=float64) 

459 # face normals need to correspond to faces 

460 if len(values) == 0 or values.shape != self.faces.shape: 

461 log.debug("face_normals incorrect shape, ignoring!") 

462 return 

463 # check if any values are larger than tol.merge 

464 # don't set the normals if they are all zero 

465 ptp = np.ptp(values) 

466 if not np.isfinite(ptp): 

467 log.debug("face_normals contain NaN, ignoring!") 

468 return 

469 if ptp < tol.merge: 

470 log.debug("face_normals all zero, ignoring!") 

471 return 

472 

473 # make sure the first few normals match the first few triangles 

474 check, valid = triangles.normals(self.vertices.view(np.ndarray)[self.faces[:20]]) 

475 compare = np.zeros((len(valid), 3)) 

476 compare[valid] = check 

477 if not np.allclose(compare, values[:20]): 

478 log.debug("face_normals didn't match triangles, ignoring!") 

479 return 

480 

481 # otherwise store face normals 

482 self._cache["face_normals"] = values 

483 

484 @property 

485 def vertices(self) -> TrackedArray: 

486 """ 

487 The vertices of the mesh. 

488 

489 This is regarded as core information which cannot be 

490 generated from cache and as such is stored in self._data 

491 which tracks the array for changes and clears cached 

492 values of the mesh if this is altered. 

493 

494 Returns 

495 ---------- 

496 vertices : (n, 3) float 

497 Points in cartesian space referenced by self.faces 

498 """ 

499 # get vertices if already stored 

500 return self._data["vertices"] 

501 

502 @vertices.setter 

503 def vertices(self, values: ArrayLike | None) -> None: 

504 """ 

505 Assign vertex values to the mesh. 

506 

507 Parameters 

508 -------------- 

509 values : (n, 3) float 

510 Points in space 

511 """ 

512 if values is None: 

513 # remove any stored data and store an empty array 

514 values = np.zeros(shape=(0, 3), dtype=float64) 

515 self._data["vertices"] = np.asanyarray(values, order="C", dtype=float64) 

516 

517 @cache_decorator 

518 def vertex_normals(self) -> NDArray[float64]: 

519 """ 

520 The vertex normals of the mesh. If the normals were loaded 

521 we check to make sure we have the same number of vertex 

522 normals and vertices before returning them. If there are 

523 no vertex normals defined or a shape mismatch we calculate 

524 the vertex normals from the mean normals of the faces the 

525 vertex is used in. 

526 

527 Returns 

528 ---------- 

529 vertex_normals : (n, 3) float 

530 Represents the surface normal at each vertex. 

531 Where n == len(self.vertices) 

532 """ 

533 # make sure we have faces_sparse 

534 return geometry.weighted_vertex_normals( 

535 vertex_count=len(self.vertices), 

536 faces=self.faces, 

537 face_normals=self.face_normals, 

538 face_angles=self.face_angles, 

539 ) 

540 

541 @vertex_normals.setter 

542 def vertex_normals(self, values: ArrayLike) -> None: 

543 """ 

544 Assign values to vertex normals. 

545 

546 Parameters 

547 ------------- 

548 values : (len(self.vertices), 3) float 

549 Unit normal vectors for each vertex 

550 """ 

551 if values is not None: 

552 values = np.asanyarray(values, order="C", dtype=float64) 

553 if values.shape == self.vertices.shape: 

554 # check to see if they assigned all zeros 

555 if np.ptp(values) < tol.merge: 

556 log.debug("vertex_normals are all zero!") 

557 self._cache["vertex_normals"] = values 

558 

559 @cache_decorator 

560 def vertex_faces(self) -> NDArray[int64]: 

561 """ 

562 A representation of the face indices that correspond to each vertex. 

563 

564 Returns 

565 ---------- 

566 vertex_faces : (n,m) int 

567 Each row contains the face indices that correspond to the given vertex, 

568 padded with -1 up to the max number of faces corresponding to any one vertex 

569 Where n == len(self.vertices), m == max number of faces for a single vertex 

570 """ 

571 vertex_faces = geometry.vertex_face_indices( 

572 vertex_count=len(self.vertices), 

573 faces=self.faces, 

574 faces_sparse=self.faces_sparse, 

575 ) 

576 return vertex_faces 

577 

578 @cache_decorator 

579 def bounds(self) -> NDArray[float64] | None: 

580 """ 

581 The axis aligned bounds of the faces of the mesh. 

582 

583 Returns 

584 ----------- 

585 bounds : (2, 3) float or None 

586 Bounding box with [min, max] coordinates 

587 If mesh is empty will return None 

588 """ 

589 # return bounds including ONLY referenced vertices 

590 in_mesh = self.vertices[self.referenced_vertices] 

591 # don't crash if we have no vertices referenced 

592 if len(in_mesh) == 0: 

593 return None 

594 # get mesh bounds with min and max 

595 return np.array([in_mesh.min(axis=0), in_mesh.max(axis=0)]) 

596 

597 @cache_decorator 

598 def extents(self) -> NDArray[float64] | None: 

599 """ 

600 The length, width, and height of the axis aligned 

601 bounding box of the mesh. 

602 

603 Returns 

604 ----------- 

605 extents : (3, ) float or None 

606 Array containing axis aligned [length, width, height] 

607 If mesh is empty returns None 

608 """ 

609 # if mesh is empty return None 

610 if self.bounds is None: 

611 return None 

612 extents = np.ptp(self.bounds, axis=0) 

613 

614 return extents 

615 

616 @cache_decorator 

617 def centroid(self) -> NDArray[float64]: 

618 """ 

619 The point in space which is the average of the triangle 

620 centroids weighted by the area of each triangle. 

621 

622 This will be valid even for non-watertight meshes, 

623 unlike self.center_mass 

624 

625 Returns 

626 ---------- 

627 centroid : (3, ) float 

628 The average vertex weighted by face area 

629 """ 

630 

631 # use the centroid of each triangle weighted by 

632 # the area of the triangle to find the overall centroid 

633 try: 

634 centroid = np.average(self.triangles_center, weights=self.area_faces, axis=0) 

635 except BaseException: 

636 # if all triangles are zero-area weights will not work 

637 centroid = self.triangles_center.mean(axis=0) 

638 return centroid 

639 

640 @property 

641 def center_mass(self) -> NDArray[float64]: 

642 """ 

643 The point in space which is the center of mass/volume. 

644 

645 Returns 

646 ----------- 

647 center_mass : (3, ) float 

648 Volumetric center of mass of the mesh. 

649 """ 

650 return self.mass_properties.center_mass 

651 

652 @center_mass.setter 

653 def center_mass(self, value: ArrayLike) -> None: 

654 """ 

655 Override the point in space which is the center of mass and volume. 

656 

657 Parameters 

658 ----------- 

659 center_mass : (3, ) float 

660 Volumetric center of mass of the mesh. 

661 """ 

662 value = np.array(value, dtype=float64) 

663 if value.shape != (3,): 

664 raise ValueError("shape must be (3,) float!") 

665 self._data["center_mass"] = value 

666 self._cache.delete("mass_properties") 

667 

668 @property 

669 def density(self) -> float: 

670 """ 

671 The density of the mesh used in inertia calculations. 

672 

673 Returns 

674 ----------- 

675 density 

676 The density of the primitive. 

677 """ 

678 return float(self.mass_properties.density) 

679 

680 @density.setter 

681 def density(self, value: Number) -> None: 

682 """ 

683 Set the density of the primitive. 

684 

685 Parameters 

686 ------------- 

687 density 

688 Specify the density of the primitive to be 

689 used in inertia calculations. 

690 """ 

691 self._data["density"] = float(value) 

692 self._cache.delete("mass_properties") 

693 

694 @property 

695 def volume(self) -> float64: 

696 """ 

697 Volume of the current mesh calculated using a surface 

698 integral. If the current mesh isn't watertight this is 

699 garbage. 

700 

701 Returns 

702 --------- 

703 volume : float 

704 Volume of the current mesh 

705 """ 

706 return self.mass_properties.volume 

707 

708 @property 

709 def mass(self) -> float64: 

710 """ 

711 Mass of the current mesh, based on specified density and 

712 volume. If the current mesh isn't watertight this is garbage. 

713 

714 Returns 

715 --------- 

716 mass : float 

717 Mass of the current mesh 

718 """ 

719 return self.mass_properties.mass 

720 

721 @property 

722 def moment_inertia(self) -> NDArray[float64]: 

723 """ 

724 Return the moment of inertia matrix of the current mesh. 

725 If mesh isn't watertight this is garbage. The returned 

726 moment of inertia is *axis aligned* at the mesh's center 

727 of mass `mesh.center_mass`. If you want the moment at any 

728 other frame including the origin call: 

729 `mesh.moment_inertia_frame` 

730 

731 Returns 

732 --------- 

733 inertia : (3, 3) float 

734 Moment of inertia of the current mesh at the center of 

735 mass and aligned with the cartesian axis. 

736 """ 

737 return self.mass_properties.inertia 

738 

739 def moment_inertia_frame(self, transform: ArrayLike) -> NDArray[float64]: 

740 """ 

741 Get the moment of inertia of this mesh with respect to 

742 an arbitrary frame, versus with respect to the center 

743 of mass as returned by `mesh.moment_inertia`. 

744 

745 For example if `transform` is an identity matrix `np.eye(4)` 

746 this will give the moment at the origin. 

747 

748 Uses the parallel axis theorum to move the center mass 

749 tensor to this arbitrary frame. 

750 

751 Parameters 

752 ------------ 

753 transform : (4, 4) float 

754 Homogeneous transformation matrix. 

755 

756 Returns 

757 ------------- 

758 inertia : (3, 3) 

759 Moment of inertia in the requested frame. 

760 """ 

761 # we'll need the inertia tensor and the center of mass 

762 props = self.mass_properties 

763 # calculated moment of inertia is at the center of mass 

764 # so we want to offset our requested translation by that 

765 # center of mass 

766 offset = np.eye(4) 

767 offset[:3, 3] = -props["center_mass"] 

768 

769 # apply the parallel axis theorum to get the new inertia 

770 return inertia.transform_inertia( 

771 inertia_tensor=props["inertia"], 

772 transform=np.dot(offset, transform), 

773 mass=props["mass"], 

774 parallel_axis=True, 

775 ) 

776 

777 @cache_decorator 

778 def principal_inertia_components(self) -> NDArray[float64]: 

779 """ 

780 Return the principal components of inertia 

781 

782 Ordering corresponds to mesh.principal_inertia_vectors 

783 

784 Returns 

785 ---------- 

786 components : (3, ) float 

787 Principal components of inertia 

788 """ 

789 # both components and vectors from inertia matrix 

790 components, vectors = inertia.principal_axis(self.moment_inertia) 

791 # store vectors in cache for later 

792 self._cache["principal_inertia_vectors"] = vectors 

793 

794 return components 

795 

796 @property 

797 def principal_inertia_vectors(self) -> NDArray[float64]: 

798 """ 

799 Return the principal axis of inertia as unit vectors. 

800 The order corresponds to `mesh.principal_inertia_components`. 

801 

802 Returns 

803 ---------- 

804 vectors : (3, 3) float 

805 Three vectors pointing along the 

806 principal axis of inertia directions 

807 """ 

808 _ = self.principal_inertia_components 

809 return self._cache["principal_inertia_vectors"] 

810 

811 @cache_decorator 

812 def principal_inertia_transform(self) -> NDArray[float64]: 

813 """ 

814 A transform which moves the current mesh so the principal 

815 inertia vectors are on the X,Y, and Z axis, and the centroid is 

816 at the origin. 

817 

818 Returns 

819 ---------- 

820 transform : (4, 4) float 

821 Homogeneous transformation matrix 

822 """ 

823 order = np.argsort(self.principal_inertia_components)[1:][::-1] 

824 vectors = self.principal_inertia_vectors[order] 

825 vectors = np.vstack((vectors, np.cross(*vectors))) 

826 

827 transform = np.eye(4) 

828 transform[:3, :3] = vectors 

829 transform = transformations.transform_around( 

830 matrix=transform, point=self.centroid 

831 ) 

832 transform[:3, 3] -= self.centroid 

833 

834 return transform 

835 

836 @cache_decorator 

837 def symmetry(self) -> str | None: 

838 """ 

839 Check whether a mesh has rotational symmetry around 

840 an axis (radial) or point (spherical). 

841 

842 Returns 

843 ----------- 

844 symmetry : None, 'radial', 'spherical' 

845 What kind of symmetry does the mesh have. 

846 """ 

847 symmetry, axis, section = inertia.radial_symmetry(self) 

848 self._cache["symmetry_axis"] = axis 

849 self._cache["symmetry_section"] = section 

850 return symmetry 

851 

852 @property 

853 def symmetry_axis(self) -> NDArray[float64] | None: 

854 """ 

855 If a mesh has rotational symmetry, return the axis. 

856 

857 Returns 

858 ------------ 

859 axis : (3, ) float 

860 Axis around which a 2D profile was revolved to create this mesh. 

861 """ 

862 if self.symmetry is None: 

863 return None 

864 return self._cache["symmetry_axis"] 

865 

866 @property 

867 def symmetry_section(self) -> NDArray[float64] | None: 

868 """ 

869 If a mesh has rotational symmetry return the two 

870 vectors which make up a section coordinate frame. 

871 

872 Returns 

873 ---------- 

874 section : (2, 3) float 

875 Vectors to take a section along 

876 """ 

877 if self.symmetry is None: 

878 return None 

879 return self._cache["symmetry_section"] 

880 

881 @cache_decorator 

882 def triangles(self) -> NDArray[float64]: 

883 """ 

884 Actual triangles of the mesh (points, not indexes) 

885 

886 Returns 

887 --------- 

888 triangles : (n, 3, 3) float 

889 Points of triangle vertices 

890 """ 

891 # use of advanced indexing on our tracked arrays will 

892 # trigger a change flag which means the hash will have to be 

893 # recomputed. We can escape this check by viewing the array. 

894 return self.vertices.view(np.ndarray)[self.faces] 

895 

896 @cache_decorator 

897 def triangles_tree(self) -> Index: 

898 """ 

899 An R-tree containing each face of the mesh. 

900 

901 Returns 

902 ---------- 

903 tree : rtree.index 

904 Each triangle in self.faces has a rectangular cell 

905 """ 

906 return triangles.bounds_tree(self.triangles) 

907 

908 @cache_decorator 

909 def triangles_center(self) -> NDArray[float64]: 

910 """ 

911 The center of each triangle (barycentric [1/3, 1/3, 1/3]) 

912 

913 Returns 

914 --------- 

915 triangles_center : (len(self.faces), 3) float 

916 Center of each triangular face 

917 """ 

918 return self.triangles.mean(axis=1) 

919 

920 @cache_decorator 

921 def triangles_cross(self) -> NDArray[float64]: 

922 """ 

923 The cross product of two edges of each triangle. 

924 

925 Returns 

926 --------- 

927 crosses : (n, 3) float 

928 Cross product of each triangle 

929 """ 

930 crosses = triangles.cross(self.triangles) 

931 return crosses 

932 

933 @cache_decorator 

934 def edges(self) -> NDArray[int64]: 

935 """ 

936 Edges of the mesh (derived from faces). 

937 

938 Returns 

939 --------- 

940 edges : (n, 2) int 

941 List of vertex indices making up edges 

942 """ 

943 edges, index = geometry.faces_to_edges( 

944 self.faces.view(np.ndarray), return_index=True 

945 ) 

946 self._cache["edges_face"] = index 

947 return edges 

948 

949 @cache_decorator 

950 def edges_face(self) -> NDArray[int64]: 

951 """ 

952 Which face does each edge belong to. 

953 

954 Returns 

955 --------- 

956 edges_face : (n, ) int 

957 Index of self.faces 

958 """ 

959 _ = self.edges 

960 return self._cache["edges_face"] 

961 

962 @cache_decorator 

963 def edges_unique(self) -> NDArray[int64]: 

964 """ 

965 The unique edges of the mesh. 

966 

967 Returns 

968 ---------- 

969 edges_unique : (n, 2) int 

970 Vertex indices for unique edges 

971 """ 

972 unique, inverse = grouping.unique_rows(self.edges_sorted) 

973 edges_unique = self.edges_sorted[unique] 

974 # edges_unique will be added automatically by the decorator 

975 # additional terms generated need to be added to the cache manually 

976 self._cache["edges_unique_idx"] = unique 

977 self._cache["edges_unique_inverse"] = inverse 

978 return edges_unique 

979 

980 @cache_decorator 

981 def edges_unique_length(self) -> NDArray[float64]: 

982 """ 

983 How long is each unique edge. 

984 

985 Returns 

986 ---------- 

987 length : (len(self.edges_unique), ) float 

988 Length of each unique edge 

989 """ 

990 vector = np.subtract(*self.vertices[self.edges_unique.T]) 

991 length = util.row_norm(vector) 

992 return length 

993 

994 @cache_decorator 

995 def edges_unique_inverse(self) -> NDArray[int64]: 

996 """ 

997 Return the inverse required to reproduce 

998 self.edges_sorted from self.edges_unique. 

999 

1000 Useful for referencing edge properties: 

1001 mesh.edges_unique[mesh.edges_unique_inverse] == m.edges_sorted 

1002 

1003 Returns 

1004 ---------- 

1005 inverse : (len(self.edges), ) int 

1006 Indexes of self.edges_unique 

1007 """ 

1008 _ = self.edges_unique 

1009 return self._cache["edges_unique_inverse"] 

1010 

1011 @cache_decorator 

1012 def edges_sorted(self) -> NDArray[int64]: 

1013 """ 

1014 Edges sorted along axis 1 

1015 

1016 Returns 

1017 ---------- 

1018 edges_sorted : (n, 2) 

1019 Same as self.edges but sorted along axis 1 

1020 """ 

1021 edges_sorted = np.sort(self.edges, axis=1) 

1022 return edges_sorted 

1023 

1024 @cache_decorator 

1025 def edges_sorted_tree(self) -> cKDTree: 

1026 """ 

1027 A KDTree for mapping edges back to edge index. 

1028 

1029 Returns 

1030 ------------ 

1031 tree : scipy.spatial.cKDTree 

1032 Tree when queried with edges will return 

1033 their index in mesh.edges_sorted 

1034 """ 

1035 return cKDTree(self.edges_sorted) 

1036 

1037 @cache_decorator 

1038 def edges_sparse(self) -> coo_matrix: 

1039 """ 

1040 Edges in sparse bool COO graph format where connected 

1041 vertices are True. 

1042 

1043 Returns 

1044 ---------- 

1045 sparse: (len(self.vertices), len(self.vertices)) bool 

1046 Sparse graph in COO format 

1047 """ 

1048 sparse = graph.edges_to_coo(self.edges, count=len(self.vertices)) 

1049 return sparse 

1050 

1051 @cache_decorator 

1052 def body_count(self) -> int: 

1053 """ 

1054 How many connected groups of vertices exist in this mesh. 

1055 Note that this number may differ from result in mesh.split, 

1056 which is calculated from FACE rather than vertex adjacency. 

1057 

1058 Returns 

1059 ----------- 

1060 count : int 

1061 Number of connected vertex groups 

1062 """ 

1063 # labels are (len(vertices), int) OB 

1064 count, labels = graph.csgraph.connected_components( 

1065 self.edges_sparse, directed=False, return_labels=True 

1066 ) 

1067 self._cache["vertices_component_label"] = labels 

1068 return count 

1069 

1070 @cache_decorator 

1071 def faces_unique_edges(self) -> NDArray[int64]: 

1072 """ 

1073 For each face return which indexes in mesh.unique_edges constructs 

1074 that face. 

1075 

1076 Returns 

1077 --------- 

1078 faces_unique_edges : (len(self.faces), 3) int 

1079 Indexes of self.edges_unique that 

1080 construct self.faces 

1081 

1082 Examples 

1083 --------- 

1084 In [0]: mesh.faces[:2] 

1085 Out[0]: 

1086 TrackedArray([[ 1, 6946, 24224], 

1087 [ 6946, 1727, 24225]]) 

1088 

1089 In [1]: mesh.edges_unique[mesh.faces_unique_edges[:2]] 

1090 Out[1]: 

1091 array([[[ 1, 6946], 

1092 [ 6946, 24224], 

1093 [ 1, 24224]], 

1094 [[ 1727, 6946], 

1095 [ 1727, 24225], 

1096 [ 6946, 24225]]]) 

1097 """ 

1098 # make sure we have populated unique edges 

1099 _ = self.edges_unique 

1100 # we are relying on the fact that edges are stacked in triplets 

1101 result = self._cache["edges_unique_inverse"].reshape((-1, 3)) 

1102 return result 

1103 

1104 @cache_decorator 

1105 def euler_number(self) -> int: 

1106 """ 

1107 Return the Euler characteristic (a topological invariant) for the mesh 

1108 In order to guarantee correctness, this should be called after 

1109 remove_unreferenced_vertices 

1110 

1111 Returns 

1112 ---------- 

1113 euler_number : int 

1114 Topological invariant 

1115 """ 

1116 return int( 

1117 self.referenced_vertices.sum() - len(self.edges_unique) + len(self.faces) 

1118 ) 

1119 

1120 @cache_decorator 

1121 def referenced_vertices(self) -> NDArray[np.bool_]: 

1122 """ 

1123 Which vertices in the current mesh are referenced by a face. 

1124 

1125 Returns 

1126 ------------- 

1127 referenced : (len(self.vertices), ) bool 

1128 Which vertices are referenced by a face 

1129 """ 

1130 referenced = np.zeros(len(self.vertices), dtype=bool) 

1131 referenced[self.faces] = True 

1132 return referenced 

1133 

1134 def convert_units(self, desired: str, guess: bool = False) -> Self: 

1135 """ 

1136 Convert the units of the mesh into a specified unit. 

1137 

1138 Parameters 

1139 ------------ 

1140 desired : string 

1141 Units to convert to (eg 'inches') 

1142 guess : boolean 

1143 If self.units are not defined should we 

1144 guess the current units of the document and then convert? 

1145 

1146 Returns 

1147 ------------ 

1148 self: trimesh.Trimesh 

1149 Current mesh 

1150 """ 

1151 units._convert_units(self, desired, guess) 

1152 return self 

1153 

1154 def merge_vertices( 

1155 self, 

1156 merge_tex: bool | None = None, 

1157 merge_norm: bool | None = None, 

1158 digits_vertex: Integer | None = None, 

1159 digits_norm: Integer | None = None, 

1160 digits_uv: Integer | None = None, 

1161 ) -> None: 

1162 """ 

1163 Removes duplicate vertices grouped by position and 

1164 optionally texture coordinate and normal. 

1165 

1166 Parameters 

1167 ------------- 

1168 merge_tex : bool 

1169 If True textured meshes with UV coordinates will 

1170 have vertices merged regardless of UV coordinates 

1171 merge_norm : bool 

1172 If True, meshes with vertex normals will have 

1173 vertices merged ignoring different normals 

1174 digits_vertex : None or int 

1175 Number of digits to consider for vertex position 

1176 digits_norm : int 

1177 Number of digits to consider for unit normals 

1178 digits_uv : int 

1179 Number of digits to consider for UV coordinates 

1180 """ 

1181 grouping.merge_vertices( 

1182 mesh=self, 

1183 merge_tex=merge_tex, 

1184 merge_norm=merge_norm, 

1185 digits_vertex=digits_vertex, 

1186 digits_norm=digits_norm, 

1187 digits_uv=digits_uv, 

1188 ) 

1189 

1190 def update_vertices( 

1191 self, 

1192 mask: ArrayLike, 

1193 inverse: ArrayLike | None = None, 

1194 ) -> None: 

1195 """ 

1196 Update vertices with a mask. 

1197 

1198 Parameters 

1199 ------------ 

1200 mask : (len(self.vertices)) bool 

1201 Array of which vertices to keep 

1202 inverse : (len(self.vertices)) int 

1203 Array to reconstruct vertex references 

1204 such as output by np.unique 

1205 """ 

1206 # if the mesh is already empty we can't remove anything 

1207 if self.is_empty: 

1208 return 

1209 

1210 # make sure mask is a numpy array 

1211 mask = np.asanyarray(mask) 

1212 

1213 if (mask.dtype.name == "bool" and mask.all()) or len(mask) == 0 or self.is_empty: 

1214 # mask doesn't remove any vertices so exit early 

1215 return 

1216 

1217 # create the inverse mask if not passed 

1218 if inverse is None: 

1219 inverse = np.zeros(len(self.vertices), dtype=int64) 

1220 if mask.dtype.kind == "b": 

1221 inverse[mask] = np.arange(mask.sum()) 

1222 elif mask.dtype.kind == "i": 

1223 inverse[mask] = np.arange(len(mask)) 

1224 else: 

1225 inverse = None 

1226 

1227 # re-index faces from inverse 

1228 if inverse is not None and util.is_shape(self.faces, (-1, 3)): 

1229 self.faces = inverse[self.faces.reshape(-1)].reshape((-1, 3)) 

1230 

1231 # update the visual object with our mask 

1232 self.visual.update_vertices(mask) 

1233 # get the normals from cache before dumping 

1234 cached_normals = self._cache["vertex_normals"] 

1235 

1236 # apply to face_attributes 

1237 count = len(self.vertices) 

1238 for key, value in self.vertex_attributes.items(): 

1239 try: 

1240 # covers un-len'd objects as well 

1241 if len(value) != count: 

1242 raise TypeError() 

1243 except TypeError: 

1244 continue 

1245 # apply the mask to the attribute 

1246 self.vertex_attributes[key] = value[mask] 

1247 

1248 # actually apply the mask 

1249 self.vertices = self.vertices[mask] 

1250 

1251 # if we had passed vertex normals try to save them 

1252 if util.is_shape(cached_normals, (-1, 3)): 

1253 try: 

1254 self.vertex_normals = cached_normals[mask] 

1255 except BaseException: 

1256 pass 

1257 

1258 def update_faces(self, mask: ArrayLike) -> None: 

1259 """ 

1260 In many cases, we will want to remove specific faces. 

1261 However, there is additional bookkeeping to do this cleanly. 

1262 This function updates the set of faces with a validity mask, 

1263 as well as keeping track of normals and colors. 

1264 

1265 Parameters 

1266 ------------ 

1267 mask : (m) int or (len(self.faces)) bool 

1268 Mask to remove faces 

1269 """ 

1270 # if the mesh is already empty we can't remove anything 

1271 if self.is_empty: 

1272 return 

1273 

1274 mask = np.asanyarray(mask) 

1275 if mask.dtype.name == "bool" and mask.all(): 

1276 # mask removes no faces so exit early 

1277 return 

1278 

1279 # try to save face normals before dumping cache 

1280 cached_normals = self._cache["face_normals"] 

1281 

1282 faces = self._data["faces"] 

1283 # if Trimesh has been subclassed and faces have been moved 

1284 # from data to cache, get faces from cache. 

1285 if not util.is_shape(faces, (-1, 3)): 

1286 faces = self._cache["faces"] 

1287 

1288 # apply to face_attributes 

1289 count = len(self.faces) 

1290 for key, value in self.face_attributes.items(): 

1291 try: 

1292 # covers un-len'd objects as well 

1293 if len(value) != count: 

1294 raise TypeError() 

1295 except TypeError: 

1296 continue 

1297 # apply the mask to the attribute 

1298 self.face_attributes[key] = value[mask] 

1299 

1300 # actually apply the mask 

1301 self.faces = faces[mask] 

1302 

1303 # apply to face colors 

1304 self.visual.update_faces(mask) 

1305 

1306 # if our normals were the correct shape apply them 

1307 if util.is_shape(cached_normals, (-1, 3)): 

1308 self.face_normals = cached_normals[mask] 

1309 

1310 def extend_faces(self, new_faces: ArrayLike): 

1311 """ 

1312 Extend `mesh.faces` in-place with new triangles. 

1313 

1314 This does substantial bookkeeping: padding face colors 

1315 and face attributes with default values, and preserving cached 

1316 face normals to avoid recomputing every normal. 

1317 

1318 Parameters 

1319 ------------ 

1320 new_faces : (n, 3) integer 

1321 The new faces as indexes of `self.vertices` 

1322 """ 

1323 new_faces = np.asanyarray(new_faces, dtype=np.int64) 

1324 if len(new_faces.shape) != 2 or new_faces.shape[1] != 3: 

1325 raise ValueError(f"Faces must be triangular, not `{new_faces.shape}`!") 

1326 

1327 if len(new_faces) == 0: 

1328 return 

1329 

1330 # make sure the cache is up-to-date 

1331 self._cache.verify() 

1332 

1333 # if we manage to extend colors and normals 

1334 extend_normals, extend_colors = None, None 

1335 

1336 # always filter degenerate triangles 

1337 new_normals, valid = triangles.normals(self.vertices[new_faces]) 

1338 new_faces = new_faces[valid] 

1339 if len(new_faces) == 0: 

1340 return 

1341 

1342 # save cached normals if available to avoid a full recompute 

1343 if "face_normals" in self._cache.cache: 

1344 cached_normals = self._cache.cache["face_normals"] 

1345 if len(cached_normals) > 0: 

1346 extend_normals = util.vstack_empty((cached_normals, new_normals)) 

1347 

1348 if self.visual.defined and self.visual.kind == "face": 

1349 extend_colors = util.vstack_empty( 

1350 ( 

1351 self.visual.face_colors, 

1352 np.tile(visual.DEFAULT_COLOR, (len(new_faces), 1)), 

1353 ) 

1354 ) 

1355 

1356 ########## 

1357 # DO ALL MUTATION AT THE END HERE 

1358 # apply the new faces 

1359 original_length = len(self._data["faces"]) 

1360 self.faces = util.vstack_empty((self._data["faces"], new_faces)) 

1361 # dump the cache to set the new hash to the stacked faces 

1362 self._cache.verify() 

1363 # save us a normals recompute if we can 

1364 if extend_normals is not None: 

1365 self._cache["face_normals"] = extend_normals 

1366 if extend_colors is not None: 

1367 self.visual.face_colors = extend_colors 

1368 

1369 # collect new, padded face attributes 

1370 new_attribs = {} 

1371 for name, attrib in self.face_attributes.items(): 

1372 shape = np.shape(attrib) 

1373 if len(shape) == 0 or shape[0] != original_length: 

1374 continue 

1375 # pad integers with -1 and everything else with zeros 

1376 fill = -1 if attrib.dtype.kind == "i" else 0 

1377 pad_shape = (len(new_faces),) + shape[1:] 

1378 pad = np.full(pad_shape, fill, dtype=attrib.dtype) 

1379 new_attribs[name] = np.concatenate((attrib, pad)) 

1380 # update outside the loop with new values 

1381 self.face_attributes.update(new_attribs) 

1382 

1383 def remove_infinite_values(self) -> None: 

1384 """ 

1385 Ensure that every vertex and face consists of finite numbers. 

1386 This will remove vertices or faces containing np.nan and np.inf 

1387 

1388 Alters `self.faces` and `self.vertices` 

1389 """ 

1390 if util.is_shape(self.faces, (-1, 3)): 

1391 # (len(self.faces), ) bool, mask for faces 

1392 face_mask = np.isfinite(self.faces).all(axis=1) 

1393 self.update_faces(face_mask) 

1394 

1395 if util.is_shape(self.vertices, (-1, 3)): 

1396 # (len(self.vertices), ) bool, mask for vertices 

1397 vertex_mask = np.isfinite(self.vertices).all(axis=1) 

1398 self.update_vertices(vertex_mask) 

1399 

1400 def unique_faces(self) -> NDArray[np.bool_]: 

1401 """ 

1402 On the current mesh find which faces are unique. 

1403 

1404 Returns 

1405 -------- 

1406 unique : (len(faces),) bool 

1407 A mask where the first occurrence of a unique face is true. 

1408 """ 

1409 mask = np.zeros(len(self.faces), dtype=bool) 

1410 mask[grouping.unique_rows(np.sort(self.faces, axis=1))[0]] = True 

1411 return mask 

1412 

1413 def rezero(self) -> None: 

1414 """ 

1415 Translate the mesh so that all vertex vertices are positive 

1416 and the lower bound of `self.bounds` will be exactly zero. 

1417 

1418 Alters `self.vertices`. 

1419 """ 

1420 self.apply_translation(self.bounds[0] * -1.0) 

1421 

1422 def split(self, **kwargs) -> list["Trimesh"]: 

1423 """ 

1424 Split a mesh into multiple meshes from face 

1425 connectivity. 

1426 

1427 If only_watertight is true it will only return 

1428 watertight meshes and will attempt to repair 

1429 single triangle or quad holes. 

1430 

1431 Parameters 

1432 ---------- 

1433 mesh : trimesh.Trimesh 

1434 The source multibody mesh to split 

1435 only_watertight 

1436 Only return watertight components and discard 

1437 any connected component that isn't fully watertight. 

1438 repair 

1439 If set try to fill small holes in a mesh, before the 

1440 discard step in `only_watertight. 

1441 adjacency : (n, 2) int 

1442 If passed will be used instead of `mesh.face_adjacency` 

1443 engine 

1444 Which graph engine to use for the connected components. 

1445 kwargs 

1446 Will be passed to `mesh.submesh` 

1447 

1448 Returns 

1449 ---------- 

1450 meshes : (m,) trimesh.Trimesh 

1451 Results of splitting based on parameters. 

1452 """ 

1453 return graph.split(self, **kwargs) 

1454 

1455 @cache_decorator 

1456 def face_adjacency(self) -> NDArray[int64]: 

1457 """ 

1458 Find faces that share an edge i.e. 'adjacent' faces. 

1459 

1460 Returns 

1461 ---------- 

1462 adjacency : (n, 2) int 

1463 Pairs of faces which share an edge 

1464 

1465 Examples 

1466 --------- 

1467 

1468 In [1]: mesh = trimesh.load('models/featuretype.STL') 

1469 

1470 In [2]: mesh.face_adjacency 

1471 Out[2]: 

1472 array([[ 0, 1], 

1473 [ 2, 3], 

1474 [ 0, 3], 

1475 ..., 

1476 [1112, 949], 

1477 [3467, 3475], 

1478 [1113, 3475]]) 

1479 

1480 In [3]: mesh.faces[mesh.face_adjacency[0]] 

1481 Out[3]: 

1482 TrackedArray([[ 1, 0, 408], 

1483 [1239, 0, 1]], dtype=int64) 

1484 

1485 In [4]: import networkx as nx 

1486 

1487 In [5]: graph = nx.from_edgelist(mesh.face_adjacency) 

1488 

1489 In [6]: groups = nx.connected_components(graph) 

1490 """ 

1491 adjacency, edges = graph.face_adjacency(mesh=self, return_edges=True) 

1492 self._cache["face_adjacency_edges"] = edges 

1493 return adjacency 

1494 

1495 @cache_decorator 

1496 def face_neighborhood(self) -> NDArray[int64]: 

1497 """ 

1498 Find faces that share a vertex i.e. 'neighbors' faces. 

1499 

1500 Returns 

1501 ---------- 

1502 neighborhood : (n, 2) int 

1503 Pairs of faces which share a vertex 

1504 """ 

1505 return graph.face_neighborhood(self) 

1506 

1507 @cache_decorator 

1508 def face_adjacency_edges(self) -> NDArray[int64]: 

1509 """ 

1510 Returns the edges that are shared by the adjacent faces. 

1511 

1512 Returns 

1513 -------- 

1514 edges : (n, 2) int 

1515 Vertex indices which correspond to face_adjacency 

1516 """ 

1517 # this value is calculated as a byproduct of the face adjacency 

1518 _ = self.face_adjacency 

1519 return self._cache["face_adjacency_edges"] 

1520 

1521 @cache_decorator 

1522 def face_adjacency_edges_tree(self) -> cKDTree: 

1523 """ 

1524 A KDTree for mapping edges back face adjacency index. 

1525 

1526 Returns 

1527 ------------ 

1528 tree : scipy.spatial.cKDTree 

1529 Tree when queried with SORTED edges will return 

1530 their index in mesh.face_adjacency 

1531 """ 

1532 return cKDTree(self.face_adjacency_edges) 

1533 

1534 @cache_decorator 

1535 def face_adjacency_angles(self) -> NDArray[float64]: 

1536 """ 

1537 Return the unsigned angle between adjacent faces 

1538 in radians. 

1539 

1540 Note that if you want a signed angle you can easily 

1541 use the `face_adjacency_convex` attribute to get a 

1542 signed angle with advanced indexing: 

1543 

1544 ``` 

1545 # get a sign per face_adacency pair from the "is it convex" boolean 

1546 signs = np.array([-1.0, 1.0])[mesh.face_adjacency_convex.astype(np.int64)] 

1547 

1548 # apply the signs to the angles 

1549 angles = mesh.face_adjacency_angles * signs 

1550 ``` 

1551 

1552 Returns 

1553 -------- 

1554 adjacency_angle : (len(self.face_adjacency), ) float 

1555 Unsigned angle between adjacent faces 

1556 corresponding with `self.face_adjacency` 

1557 """ 

1558 # get pairs of unit vectors for adjacent faces 

1559 pairs = self.face_normals[self.face_adjacency] 

1560 # find the angle between the pairs of vectors 

1561 angles = geometry.vector_angle(pairs) 

1562 return angles 

1563 

1564 @cache_decorator 

1565 def face_adjacency_projections(self) -> NDArray[float64]: 

1566 """ 

1567 The projection of the non-shared vertex of a triangle onto 

1568 its adjacent face 

1569 

1570 Returns 

1571 ---------- 

1572 projections : (len(self.face_adjacency), ) float 

1573 Dot product of vertex 

1574 onto plane of adjacent triangle. 

1575 """ 

1576 projections = convex.adjacency_projections(self) 

1577 return projections 

1578 

1579 @cache_decorator 

1580 def face_adjacency_convex(self) -> NDArray[np.bool_]: 

1581 """ 

1582 Return faces which are adjacent and locally convex. 

1583 

1584 What this means is that given faces A and B, the one vertex 

1585 in B that is not shared with A, projected onto the plane of A 

1586 has a projection that is zero or negative. 

1587 

1588 Returns 

1589 ---------- 

1590 are_convex : (len(self.face_adjacency), ) bool 

1591 Face pairs that are locally convex 

1592 """ 

1593 return self.face_adjacency_projections < tol.merge 

1594 

1595 @cache_decorator 

1596 def face_adjacency_unshared(self) -> NDArray[int64]: 

1597 """ 

1598 Return the vertex index of the two vertices not in the shared 

1599 edge between two adjacent faces 

1600 

1601 Returns 

1602 ----------- 

1603 vid_unshared : (len(mesh.face_adjacency), 2) int 

1604 Indexes of mesh.vertices 

1605 """ 

1606 return graph.face_adjacency_unshared(self) 

1607 

1608 @cache_decorator 

1609 def face_adjacency_radius(self) -> NDArray[float64]: 

1610 """ 

1611 The approximate radius of a cylinder that fits inside adjacent faces. 

1612 

1613 Returns 

1614 ------------ 

1615 radii : (len(self.face_adjacency), ) float 

1616 Approximate radius formed by triangle pair 

1617 """ 

1618 radii, self._cache["face_adjacency_span"] = graph.face_adjacency_radius(mesh=self) 

1619 return radii 

1620 

1621 @cache_decorator 

1622 def face_adjacency_span(self) -> NDArray[float64]: 

1623 """ 

1624 The approximate perpendicular projection of the non-shared 

1625 vertices in a pair of adjacent faces onto the shared edge of 

1626 the two faces. 

1627 

1628 Returns 

1629 ------------ 

1630 span : (len(self.face_adjacency), ) float 

1631 Approximate span between the non-shared vertices 

1632 """ 

1633 _ = self.face_adjacency_radius 

1634 return self._cache["face_adjacency_span"] 

1635 

1636 @cache_decorator 

1637 def integral_mean_curvature(self) -> float64: 

1638 """ 

1639 The integral mean curvature, or the surface integral of the mean curvature. 

1640 

1641 Returns 

1642 --------- 

1643 area : float 

1644 Integral mean curvature of mesh 

1645 """ 

1646 edges_length = np.linalg.norm( 

1647 np.subtract(*self.vertices[self.face_adjacency_edges.T]), axis=1 

1648 ) 

1649 # assign signs based on convex adjacency of face pairs 

1650 signs = np.array([-1.0, 1.0])[self.face_adjacency_convex.astype(np.int64)] 

1651 # adjust face adjacency angles with signs to reflect orientation 

1652 angles = self.face_adjacency_angles * signs 

1653 return (angles * edges_length).sum() * 0.5 

1654 

1655 @cache_decorator 

1656 def vertex_adjacency_graph(self) -> Graph: 

1657 """ 

1658 Returns a networkx graph representing the vertices and their connections 

1659 in the mesh. 

1660 

1661 Returns 

1662 --------- 

1663 graph: networkx.Graph 

1664 Graph representing vertices and edges between 

1665 them where vertices are nodes and edges are edges 

1666 

1667 Examples 

1668 ---------- 

1669 This is useful for getting nearby vertices for a given vertex, 

1670 potentially for some simple smoothing techniques. 

1671 

1672 mesh = trimesh.primitives.Box() 

1673 graph = mesh.vertex_adjacency_graph 

1674 graph.neighbors(0) 

1675 > [1, 2, 3, 4] 

1676 """ 

1677 

1678 return graph.vertex_adjacency_graph(mesh=self) 

1679 

1680 @cache_decorator 

1681 def vertex_neighbors(self) -> list[list[int64]]: 

1682 """ 

1683 The vertex neighbors of each vertex of the mesh, determined from 

1684 the cached vertex_adjacency_graph, if already existent. 

1685 

1686 Returns 

1687 ---------- 

1688 vertex_neighbors : (len(self.vertices), ) int 

1689 Represents immediate neighbors of each vertex along 

1690 the edge of a triangle 

1691 

1692 Examples 

1693 ---------- 

1694 This is useful for getting nearby vertices for a given vertex, 

1695 potentially for some simple smoothing techniques. 

1696 

1697 >>> mesh = trimesh.primitives.Box() 

1698 >>> mesh.vertex_neighbors[0] 

1699 [1, 2, 3, 4] 

1700 """ 

1701 return graph.neighbors(edges=self.edges_unique, max_index=len(self.vertices)) 

1702 

1703 @cache_decorator 

1704 def is_winding_consistent(self) -> bool: 

1705 """ 

1706 Does the mesh have consistent winding or not. 

1707 A mesh with consistent winding has each shared edge 

1708 going in an opposite direction from the other in the pair. 

1709 

1710 Returns 

1711 -------- 

1712 consistent : bool 

1713 Is winding is consistent or not 

1714 """ 

1715 if self.is_empty: 

1716 return False 

1717 # consistent winding check is populated into the cache by is_watertight 

1718 _ = self.is_watertight 

1719 return self._cache["is_winding_consistent"] 

1720 

1721 @cache_decorator 

1722 def is_watertight(self) -> bool: 

1723 """ 

1724 Check if a mesh is watertight by making sure every edge is 

1725 included in two faces. 

1726 

1727 Returns 

1728 ---------- 

1729 is_watertight : bool 

1730 Is mesh watertight or not 

1731 """ 

1732 if self.is_empty: 

1733 return False 

1734 watertight, winding = graph.is_watertight( 

1735 edges=self.edges, edges_sorted=self.edges_sorted 

1736 ) 

1737 self._cache["is_winding_consistent"] = winding 

1738 return watertight 

1739 

1740 @cache_decorator 

1741 def is_volume(self) -> bool: 

1742 """ 

1743 Check if a mesh has all the properties required to represent 

1744 a valid volume, rather than just a surface. 

1745 

1746 These properties include being watertight, having consistent 

1747 winding and outward facing normals. 

1748 

1749 Returns 

1750 --------- 

1751 valid 

1752 Does the mesh represent a volume 

1753 """ 

1754 return bool( 

1755 self.is_watertight 

1756 and self.is_winding_consistent 

1757 and np.isfinite(self.center_mass).all() 

1758 and self.volume > 0.0 

1759 ) 

1760 

1761 @property 

1762 def is_empty(self) -> bool: 

1763 """ 

1764 Does the current mesh have data defined. 

1765 

1766 Returns 

1767 -------- 

1768 empty : bool 

1769 If True, no data is set on the current mesh 

1770 """ 

1771 return self._data.is_empty() 

1772 

1773 @cache_decorator 

1774 def is_convex(self) -> bool: 

1775 """ 

1776 Check if a mesh is convex or not. 

1777 

1778 Returns 

1779 ---------- 

1780 is_convex: bool 

1781 Is mesh convex or not 

1782 """ 

1783 if self.is_empty: 

1784 return False 

1785 

1786 is_convex = bool(convex.is_convex(self)) 

1787 return is_convex 

1788 

1789 @cache_decorator 

1790 def kdtree(self) -> cKDTree: 

1791 """ 

1792 Return a scipy.spatial.cKDTree of the vertices of the mesh. 

1793 Not cached as this lead to observed memory issues and segfaults. 

1794 

1795 Returns 

1796 --------- 

1797 tree : scipy.spatial.cKDTree 

1798 Contains mesh.vertices 

1799 """ 

1800 return cKDTree(self.vertices.view(np.ndarray)) 

1801 

1802 def nondegenerate_faces(self, height: Floating = tol.merge) -> NDArray[np.bool_]: 

1803 """ 

1804 Identify degenerate faces (faces without 3 unique vertex indices) 

1805 in the current mesh. 

1806 

1807 Usage example for removing them: 

1808 `mesh.update_faces(mesh.nondegenerate_faces())` 

1809 

1810 If a height is specified, it will identify any face with a 2D oriented 

1811 bounding box with one edge shorter than that height. 

1812 

1813 If not specified, it will identify any face with a zero normal. 

1814 

1815 Parameters 

1816 ------------ 

1817 height : float 

1818 If specified identifies faces with an oriented bounding 

1819 box shorter than this on one side. 

1820 

1821 Returns 

1822 ------------- 

1823 nondegenerate : (len(self.faces), ) bool 

1824 Mask that can be used to remove faces 

1825 """ 

1826 return triangles.nondegenerate( 

1827 self.triangles, areas=self.area_faces, height=height 

1828 ) 

1829 

1830 @cache_decorator 

1831 def facets(self) -> list[NDArray[int64]]: 

1832 """ 

1833 Return a list of face indices for coplanar adjacent faces. 

1834 

1835 Returns 

1836 --------- 

1837 facets : (n, ) sequence of (m, ) int 

1838 Groups of indexes of self.faces 

1839 """ 

1840 facets = graph.facets(self) 

1841 return facets 

1842 

1843 @cache_decorator 

1844 def facets_area(self) -> NDArray[float64]: 

1845 """ 

1846 Return an array containing the area of each facet. 

1847 

1848 Returns 

1849 --------- 

1850 area : (len(self.facets), ) float 

1851 Total area of each facet (group of faces) 

1852 """ 

1853 # avoid thrashing the cache inside a loop 

1854 area_faces = self.area_faces 

1855 # sum the area of each group of faces represented by facets 

1856 # use native python sum in tight loop as opposed to array.sum() 

1857 # as in this case the lower function call overhead of 

1858 # native sum provides roughly a 50% speedup 

1859 areas = np.array([sum(area_faces[i]) for i in self.facets], dtype=float64) 

1860 return areas 

1861 

1862 @cache_decorator 

1863 def facets_normal(self) -> NDArray[float64]: 

1864 """ 

1865 Return the normal of each facet 

1866 

1867 Returns 

1868 --------- 

1869 normals: (len(self.facets), 3) float 

1870 A unit normal vector for each facet 

1871 """ 

1872 if len(self.facets) == 0: 

1873 return np.array([]) 

1874 

1875 area_faces = self.area_faces 

1876 

1877 # the face index of the largest face in each facet 

1878 index = np.array([i[area_faces[i].argmax()] for i in self.facets]) 

1879 # (n, 3) float, unit normal vectors of facet plane 

1880 normals = self.face_normals[index] 

1881 # (n, 3) float, points on facet plane 

1882 origins = self.vertices[self.faces[:, 0][index]] 

1883 # save origins in cache 

1884 self._cache["facets_origin"] = origins 

1885 

1886 return normals 

1887 

1888 @cache_decorator 

1889 def facets_origin(self) -> NDArray[float64]: 

1890 """ 

1891 Return a point on the facet plane. 

1892 

1893 Returns 

1894 ------------ 

1895 origins : (len(self.facets), 3) float 

1896 A point on each facet plane 

1897 """ 

1898 _ = self.facets_normal 

1899 return self._cache["facets_origin"] 

1900 

1901 @cache_decorator 

1902 def facets_boundary(self) -> list[NDArray[int64]]: 

1903 """ 

1904 Return the edges which represent the boundary of each facet 

1905 

1906 Returns 

1907 --------- 

1908 edges_boundary : sequence of (n, 2) int 

1909 Indices of self.vertices 

1910 """ 

1911 # make each row correspond to a single face 

1912 edges = self.edges_sorted.reshape((-1, 6)) 

1913 # get the edges for each facet 

1914 edges_facet = [edges[i].reshape((-1, 2)) for i in self.facets] 

1915 edges_boundary = [i[grouping.group_rows(i, require_count=1)] for i in edges_facet] 

1916 return edges_boundary 

1917 

1918 @cache_decorator 

1919 def facets_on_hull(self) -> NDArray[np.bool_]: 

1920 """ 

1921 Find which facets of the mesh are on the convex hull. 

1922 

1923 Returns 

1924 --------- 

1925 on_hull : (len(mesh.facets), ) bool 

1926 is A facet on the meshes convex hull or not 

1927 """ 

1928 # if no facets exit early 

1929 if len(self.facets) == 0: 

1930 return np.array([], dtype=bool) 

1931 

1932 # facets plane, origin and normal 

1933 normals = self.facets_normal 

1934 origins = self.facets_origin 

1935 

1936 # (n, 3) convex hull vertices 

1937 convex = self.convex_hull.vertices.view(np.ndarray).copy() 

1938 

1939 # boolean mask for which facets are on convex hull 

1940 on_hull = np.zeros(len(self.facets), dtype=bool) 

1941 

1942 for i, normal, origin in zip(range(len(normals)), normals, origins): 

1943 # a facet plane is on the convex hull if every vertex 

1944 # of the convex hull is behind that plane 

1945 # which we are checking with dot products 

1946 dot = np.dot(normal, (convex - origin).T) 

1947 on_hull[i] = (dot < tol.merge).all() 

1948 

1949 return on_hull 

1950 

1951 def fix_normals(self, multibody: bool | None = None) -> Self: 

1952 """ 

1953 Find and fix problems with self.face_normals and self.faces 

1954 winding direction. 

1955 

1956 For face normals ensure that vectors are consistently pointed 

1957 outwards, and that self.faces is wound in the correct direction 

1958 for all connected components. 

1959 

1960 Parameters 

1961 ------------- 

1962 multibody : None or bool 

1963 Fix normals across multiple bodies or if unspecified 

1964 check the current `Trimesh.body_count`. 

1965 """ 

1966 if multibody is None: 

1967 multibody = self.body_count > 1 

1968 repair.fix_normals(self, multibody=multibody) 

1969 return self 

1970 

1971 def fill_holes(self) -> bool: 

1972 """ 

1973 Fill single triangle and single quad holes in the current mesh. 

1974 

1975 Returns 

1976 ---------- 

1977 watertight : bool 

1978 Is the mesh watertight after the function completes 

1979 """ 

1980 return repair.fill_holes(self) 

1981 

1982 def register( 

1983 self, other: Geometry3D | NDArray, **kwargs 

1984 ) -> tuple[NDArray[float64], float64]: 

1985 """ 

1986 Align a mesh with another mesh or a PointCloud using 

1987 the principal axes of inertia as a starting point which 

1988 is refined by iterative closest point. 

1989 

1990 Parameters 

1991 ------------ 

1992 other : trimesh.Trimesh or (n, 3) float 

1993 Mesh or points in space 

1994 samples : int 

1995 Number of samples from mesh surface to align 

1996 icp_first : int 

1997 How many ICP iterations for the 9 possible 

1998 combinations of 

1999 icp_final : int 

2000 How many ICP itertations for the closest 

2001 candidate from the wider search 

2002 

2003 Returns 

2004 ----------- 

2005 mesh_to_other : (4, 4) float 

2006 Transform to align mesh to the other object 

2007 cost : float 

2008 Average square distance per point 

2009 """ 

2010 mesh_to_other, cost = registration.mesh_other(mesh=self, other=other, **kwargs) 

2011 return mesh_to_other, cost 

2012 

2013 def compute_stable_poses( 

2014 self, 

2015 center_mass: NDArray[float64] | None = None, 

2016 sigma: Floating = 0.0, 

2017 n_samples: Integer = 1, 

2018 threshold: Floating = 0.0, 

2019 ) -> tuple[NDArray[float64], NDArray[float64]]: 

2020 """ 

2021 Computes stable orientations of a mesh and their quasi-static probabilities. 

2022 

2023 This method samples the location of the center of mass from a multivariate 

2024 gaussian (mean at com, cov equal to identity times sigma) over n_samples. 

2025 For each sample, it computes the stable resting poses of the mesh on a 

2026 a planar workspace and evaluates the probabilities of landing in 

2027 each pose if the object is dropped onto the table randomly. 

2028 

2029 This method returns the 4x4 homogeneous transform matrices that place 

2030 the shape against the planar surface with the z-axis pointing upwards 

2031 and a list of the probabilities for each pose. 

2032 The transforms and probabilities that are returned are sorted, with the 

2033 most probable pose first. 

2034 

2035 Parameters 

2036 ------------ 

2037 center_mass : (3, ) float 

2038 The object center of mass (if None, this method 

2039 assumes uniform density and watertightness and 

2040 computes a center of mass explicitly) 

2041 sigma : float 

2042 The covariance for the multivariate gaussian used 

2043 to sample center of mass locations 

2044 n_samples : int 

2045 The number of samples of the center of mass location 

2046 threshold : float 

2047 The probability value at which to threshold 

2048 returned stable poses 

2049 

2050 Returns 

2051 ------- 

2052 transforms : (n, 4, 4) float 

2053 The homogeneous matrices that transform the 

2054 object to rest in a stable pose, with the 

2055 new z-axis pointing upwards from the table 

2056 and the object just touching the table. 

2057 

2058 probs : (n, ) float 

2059 A probability ranging from 0.0 to 1.0 for each pose 

2060 """ 

2061 return poses.compute_stable_poses( 

2062 mesh=self, 

2063 center_mass=center_mass, 

2064 sigma=sigma, 

2065 n_samples=n_samples, 

2066 threshold=threshold, 

2067 ) 

2068 

2069 def subdivide( 

2070 self, face_index: ArrayLike | None = None, iterations: Integer | None = None 

2071 ) -> "Trimesh": 

2072 """ 

2073 Subdivide a mesh with each subdivided face replaced 

2074 with four smaller faces. Will return a copy of current 

2075 mesh with subdivided faces. 

2076 

2077 Parameters 

2078 ------------ 

2079 face_index : (m, ) int or None 

2080 If None all faces of mesh will be subdivided 

2081 If (m, ) int array of indices: only specified faces will be 

2082 subdivided. Note that in this case the mesh will generally 

2083 no longer be manifold, as the additional vertex on the midpoint 

2084 will not be used by the adjacent faces to the faces specified, 

2085 and an additional postprocessing step will be required to 

2086 make resulting mesh watertight 

2087 iterations : int 

2088 If passed will run subdivisions multiple times recursively. 

2089 NOT COMPATIBLE with `face_index` and will raise a `ValueError` 

2090 if both arguments are passed. 

2091 

2092 Returns 

2093 ------------ 

2094 mesh: trimesh.Trimesh 

2095 The copy of current mesh with subdivided faces. 

2096 """ 

2097 if iterations is not None: 

2098 # check that our arguments are executable 

2099 if face_index is not None: 

2100 raise ValueError("Unable to subdivide a subset with multiple iterations!") 

2101 # decrement the next iteration 

2102 next_iteration = iterations - 1 

2103 # if we've reached zero exit 

2104 if next_iteration <= 0: 

2105 next_iteration = None 

2106 

2107 visual = None 

2108 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == ( 

2109 len(self.vertices), 

2110 2, 

2111 ): 

2112 # uv coords divided along with vertices 

2113 vertices, faces, attr = remesh.subdivide( 

2114 vertices=np.hstack((self.vertices, self.visual.uv)), 

2115 faces=self.faces, 

2116 face_index=face_index, 

2117 vertex_attributes=self.vertex_attributes, 

2118 ) 

2119 

2120 # get a copy of the current visuals 

2121 visual = self.visual.copy() 

2122 

2123 # separate uv coords and vertices 

2124 vertices, visual.uv = vertices[:, :3], vertices[:, 3:] 

2125 

2126 else: 

2127 # perform the subdivision with vertex attributes 

2128 vertices, faces, attr = remesh.subdivide( 

2129 vertices=self.vertices, 

2130 faces=self.faces, 

2131 face_index=face_index, 

2132 vertex_attributes=self.vertex_attributes, 

2133 ) 

2134 

2135 # create a new mesh 

2136 result = Trimesh( 

2137 vertices=vertices, 

2138 faces=faces, 

2139 visual=visual, 

2140 vertex_attributes=attr, 

2141 process=False, 

2142 ) 

2143 

2144 if iterations is not None: 

2145 return result.subdivide(iterations=next_iteration) 

2146 

2147 return result 

2148 

2149 def subdivide_to_size( 

2150 self, max_edge: Number, max_iter: Integer = 10, return_index: bool = False 

2151 ) -> "Trimesh | tuple[Trimesh, NDArray[int64]]": 

2152 """ 

2153 Subdivide a mesh until every edge is shorter than a 

2154 specified length. 

2155 

2156 Will return a triangle soup, not a nicely structured mesh. 

2157 

2158 Parameters 

2159 ------------ 

2160 max_edge 

2161 Maximum length of any edge in the result 

2162 max_iter : int 

2163 The maximum number of times to run subdivision 

2164 return_index : bool 

2165 If True, return index of original face for new faces 

2166 

2167 Returns 

2168 ------------ 

2169 mesh: trimesh.Trimesh 

2170 The copy of current mesh with subdivided faces. 

2171 """ 

2172 # subdivide vertex attributes 

2173 visual = None 

2174 if hasattr(self.visual, "uv") and np.shape(self.visual.uv) == ( 

2175 len(self.vertices), 

2176 2, 

2177 ): 

2178 # uv coords divided along with vertices 

2179 vertices_faces = remesh.subdivide_to_size( 

2180 vertices=np.hstack((self.vertices, self.visual.uv)), 

2181 faces=self.faces, 

2182 max_edge=max_edge, 

2183 max_iter=max_iter, 

2184 return_index=return_index, 

2185 ) 

2186 # unpack result 

2187 if return_index: 

2188 vertices, faces, final_index = vertices_faces 

2189 else: 

2190 vertices, faces = vertices_faces 

2191 

2192 # get a copy of the current visuals 

2193 visual = self.visual.copy() 

2194 

2195 # separate uv coords and vertices 

2196 vertices, visual.uv = vertices[:, :3], vertices[:, 3:] 

2197 

2198 else: 

2199 # uv coords divided along with vertices 

2200 vertices_faces = remesh.subdivide_to_size( 

2201 vertices=self.vertices, 

2202 faces=self.faces, 

2203 max_edge=max_edge, 

2204 max_iter=max_iter, 

2205 return_index=return_index, 

2206 ) 

2207 # unpack result 

2208 if return_index: 

2209 vertices, faces, final_index = vertices_faces 

2210 else: 

2211 vertices, faces = vertices_faces 

2212 

2213 # create a new mesh 

2214 result = Trimesh(vertices=vertices, faces=faces, visual=visual, process=False) 

2215 

2216 if return_index: 

2217 return result, final_index 

2218 

2219 return result 

2220 

2221 def subdivide_loop(self, iterations: Integer | None = None) -> "Trimesh": 

2222 """ 

2223 Subdivide a mesh by dividing each triangle into four 

2224 triangles and approximating their smoothed surface 

2225 using loop subdivision. Loop subdivision often looks 

2226 better on triangular meshes than catmul-clark, which 

2227 operates primarily on quads. 

2228 

2229 Parameters 

2230 ------------ 

2231 iterations : int 

2232 Number of iterations to run subdivision. 

2233 multibody : bool 

2234 If True will try to subdivide for each submesh 

2235 

2236 Returns 

2237 ------------ 

2238 mesh: trimesh.Trimesh 

2239 The copy of current mesh with subdivided faces. 

2240 """ 

2241 # perform subdivision for one mesh 

2242 new_vertices, new_faces = remesh.subdivide_loop( 

2243 vertices=self.vertices, faces=self.faces, iterations=iterations 

2244 ) 

2245 return Trimesh(vertices=new_vertices, faces=new_faces, process=False) 

2246 

2247 @property 

2248 def smooth_shaded(self) -> "Trimesh": 

2249 """ 

2250 Smooth shading in OpenGL relies on which vertices are shared, 

2251 this function will disconnect regions above an angle threshold 

2252 and return a non-watertight version which will look better 

2253 in an OpenGL rendering context. 

2254 

2255 If you would like to use non-default arguments see `graph.smooth_shade`. 

2256 

2257 Returns 

2258 --------- 

2259 smooth_shaded : trimesh.Trimesh 

2260 Non watertight version of current mesh. 

2261 """ 

2262 # key this also by the visual properties 

2263 # but store it in the mesh cache 

2264 self.visual._verify_hash() 

2265 cache = self.visual._cache 

2266 # needs to be dumped whenever visual or mesh changes 

2267 key = f"smooth_shaded_{hash(self.visual)}_{hash(self)}" 

2268 if key in cache: 

2269 return cache[key] 

2270 smooth = graph.smooth_shade(self) 

2271 # store it in the mesh cache which dumps when vertices change 

2272 cache[key] = smooth 

2273 return smooth 

2274 

2275 @property 

2276 def visual(self) -> ColorVisuals | TextureVisuals | None: 

2277 """ 

2278 Get the stored visuals for the current mesh. 

2279 

2280 Returns 

2281 ------------- 

2282 visual : ColorVisuals or TextureVisuals 

2283 Contains visual information about the mesh 

2284 """ 

2285 if hasattr(self, "_visual"): 

2286 return self._visual 

2287 return None 

2288 

2289 @visual.setter 

2290 def visual(self, value: ColorVisuals | TextureVisuals | None) -> None: 

2291 """ 

2292 When setting a visual object, always make sure 

2293 that `visual.mesh` points back to the source mesh. 

2294 

2295 Parameters 

2296 -------------- 

2297 visual : ColorVisuals or TextureVisuals 

2298 Contains visual information about the mesh 

2299 """ 

2300 if value is None: 

2301 value = ColorVisuals() 

2302 value.mesh = self 

2303 self._visual = value 

2304 

2305 def section( 

2306 self, plane_normal: ArrayLike, plane_origin: ArrayLike, **kwargs 

2307 ) -> Path3D | None: 

2308 """ 

2309 Returns a 3D cross section of the current mesh and a plane 

2310 defined by origin and normal. 

2311 

2312 Parameters 

2313 ------------ 

2314 plane_normal : (3,) float 

2315 Normal vector of section plane. 

2316 plane_origin : (3, ) float 

2317 Point on the cross section plane. 

2318 

2319 Returns 

2320 --------- 

2321 intersections 

2322 Curve of intersection or None if it was not hit by plane. 

2323 """ 

2324 # turn line segments into Path2D/Path3D objects 

2325 from .path.exchange.misc import lines_to_path 

2326 from .path.path import Path3D 

2327 

2328 # return a single cross section in 3D 

2329 lines, _face_index = intersections.mesh_plane( 

2330 mesh=self, 

2331 plane_normal=plane_normal, 

2332 plane_origin=plane_origin, 

2333 return_faces=True, 

2334 **kwargs, 

2335 ) 

2336 

2337 # if the section didn't hit the mesh return None 

2338 if len(lines) == 0: 

2339 return None 

2340 

2341 # otherwise load the line segments into the keyword arguments 

2342 # for a Path3D object. 

2343 path = lines_to_path(lines) 

2344 

2345 # add the face index info into metadata 

2346 # path.metadata["face_index"] = face_index 

2347 

2348 return Path3D(**path) 

2349 

2350 def section_multiplane( 

2351 self, 

2352 plane_origin: ArrayLike, 

2353 plane_normal: ArrayLike, 

2354 heights: ArrayLike, 

2355 ) -> list[Path2D | None]: 

2356 """ 

2357 Return multiple parallel cross sections of the current 

2358 mesh in 2D. 

2359 

2360 Parameters 

2361 ------------ 

2362 plane_origin : (3, ) float 

2363 Point on the cross section plane 

2364 plane_normal : (3) float 

2365 Normal vector of section plane 

2366 heights : (n, ) float 

2367 Each section is offset by height along 

2368 the plane normal. 

2369 

2370 Returns 

2371 --------- 

2372 paths : (n, ) Path2D or None 

2373 2D cross sections at specified heights. 

2374 path.metadata['to_3D'] contains transform 

2375 to return 2D section back into 3D space. 

2376 """ 

2377 # turn line segments into Path2D/Path3D objects 

2378 from .exchange.load import load_path 

2379 

2380 # do a multiplane intersection 

2381 lines, transforms, faces = intersections.mesh_multiplane( 

2382 mesh=self, 

2383 plane_normal=plane_normal, 

2384 plane_origin=plane_origin, 

2385 heights=heights, 

2386 ) 

2387 

2388 # turn the line segments into Path2D objects 

2389 paths = [None] * len(lines) 

2390 for i, f, segments, T in zip(range(len(lines)), faces, lines, transforms): 

2391 if len(segments) > 0: 

2392 paths[i] = load_path(segments, metadata={"to_3D": T, "face_index": f}) 

2393 return paths 

2394 

2395 def slice_plane( 

2396 self, 

2397 plane_origin: ArrayLike, 

2398 plane_normal: ArrayLike, 

2399 cap: bool = False, 

2400 face_index: ArrayLike | None = None, 

2401 **kwargs, 

2402 ) -> "Trimesh": 

2403 """ 

2404 Slice the mesh with a plane, returning a new mesh that is the 

2405 portion of the original mesh to the positive normal side of the plane 

2406 

2407 plane_origin : (3,) float 

2408 Point on plane to intersect with mesh 

2409 plane_normal : (3,) float 

2410 Normal vector of plane to intersect with mesh 

2411 cap : bool 

2412 If True, cap the result with a triangulated polygon 

2413 face_index : ((m,) int) 

2414 Indexes of mesh.faces to slice. When no mask is 

2415 provided, the default is to slice all faces. 

2416 

2417 Returns 

2418 --------- 

2419 new_mesh: trimesh.Trimesh or None 

2420 Subset of current mesh that intersects the half plane 

2421 to the positive normal side of the plane 

2422 """ 

2423 

2424 # return a new mesh 

2425 new_mesh = intersections.slice_mesh_plane( 

2426 mesh=self, 

2427 plane_normal=plane_normal, 

2428 plane_origin=plane_origin, 

2429 cap=cap, 

2430 face_index=face_index, 

2431 **kwargs, 

2432 ) 

2433 

2434 return new_mesh 

2435 

2436 def unwrap(self, image=None) -> "Trimesh": 

2437 """ 

2438 Returns a Trimesh object equivalent to the current mesh where 

2439 the vertices have been assigned uv texture coordinates. Vertices 

2440 may be split into as many as necessary by the unwrapping 

2441 algorithm, depending on how many uv maps they appear in. 

2442 

2443 Requires `pip install xatlas` 

2444 

2445 Parameters 

2446 ------------ 

2447 image : None or PIL.Image 

2448 Image to assign to the material 

2449 

2450 Returns 

2451 -------- 

2452 unwrapped : trimesh.Trimesh 

2453 Mesh with unwrapped uv coordinates 

2454 """ 

2455 import xatlas 

2456 

2457 vmap, faces, uv = xatlas.parametrize(self.vertices, self.faces) 

2458 

2459 result = Trimesh( 

2460 vertices=self.vertices[vmap], 

2461 faces=faces, 

2462 visual=TextureVisuals(uv=uv, image=image), 

2463 process=False, 

2464 ) 

2465 

2466 # run additional checks for unwrapping 

2467 if tol.strict: 

2468 # check the export object to make sure we didn't 

2469 # move the indices around on creation 

2470 assert np.allclose(result.visual.uv, uv) 

2471 assert np.allclose(result.faces, faces) 

2472 assert np.allclose(result.vertices, self.vertices[vmap]) 

2473 # check to make sure indices are still the 

2474 # same order after we've exported to OBJ 

2475 export = result.export(file_type="obj") 

2476 uv_recon = np.array( 

2477 [L[3:].split() for L in str.splitlines(export) if L.startswith("vt ")], 

2478 dtype=float64, 

2479 ) 

2480 assert np.allclose(uv_recon, uv) 

2481 v_recon = np.array( 

2482 [L[2:].split() for L in str.splitlines(export) if L.startswith("v ")], 

2483 dtype=float64, 

2484 ) 

2485 assert np.allclose(v_recon, self.vertices[vmap]) 

2486 

2487 return result 

2488 

2489 @cache_decorator 

2490 def convex_hull(self) -> "Trimesh": 

2491 """ 

2492 Returns a Trimesh object representing the convex hull of 

2493 the current mesh. 

2494 

2495 Returns 

2496 -------- 

2497 convex : trimesh.Trimesh 

2498 Mesh of convex hull of current mesh 

2499 """ 

2500 return convex.convex_hull(self) 

2501 

2502 def sample( 

2503 self, 

2504 count: Integer, 

2505 return_index: bool = False, 

2506 face_weight: NDArray[float64] | None = None, 

2507 ): 

2508 """ 

2509 Return random samples distributed across the 

2510 surface of the mesh 

2511 

2512 Parameters 

2513 ------------ 

2514 count : int 

2515 Number of points to sample 

2516 return_index : bool 

2517 If True will also return the index of which face each 

2518 sample was taken from. 

2519 face_weight : None or len(mesh.faces) float 

2520 Weight faces by a factor other than face area. 

2521 If None will be the same as face_weight=mesh.area 

2522 

2523 Returns 

2524 --------- 

2525 samples : (count, 3) float 

2526 Points on surface of mesh 

2527 face_index : (count, ) int 

2528 Index of self.faces 

2529 """ 

2530 samples, index = sample.sample_surface( 

2531 mesh=self, count=count, face_weight=face_weight 

2532 ) 

2533 if return_index: 

2534 return samples, index 

2535 return samples 

2536 

2537 def remove_unreferenced_vertices(self) -> None: 

2538 """ 

2539 Remove all vertices in the current mesh which are not 

2540 referenced by a face. 

2541 """ 

2542 referenced = np.zeros(len(self.vertices), dtype=bool) 

2543 referenced[self.faces] = True 

2544 

2545 inverse = np.zeros(len(self.vertices), dtype=int64) 

2546 inverse[referenced] = np.arange(referenced.sum()) 

2547 

2548 self.update_vertices(mask=referenced, inverse=inverse) 

2549 

2550 def unmerge_vertices(self) -> None: 

2551 """ 

2552 Removes all face references so that every face contains 

2553 three unique vertex indices and no faces are adjacent. 

2554 """ 

2555 # new faces are incrementing so every vertex is unique 

2556 faces = np.arange(len(self.faces) * 3, dtype=int64).reshape((-1, 3)) 

2557 

2558 # use update_vertices to apply mask to 

2559 # all properties that are per-vertex 

2560 self.update_vertices(self.faces.reshape(-1)) 

2561 # set faces to incrementing indexes 

2562 self.faces = faces 

2563 # keep face normals as the haven't changed 

2564 self._cache.clear(exclude=["face_normals"]) 

2565 

2566 def apply_transform(self, matrix: ArrayLike) -> Self: 

2567 """ 

2568 Transform mesh by a homogeneous transformation matrix. 

2569 

2570 Does the bookkeeping to avoid recomputing things so this function 

2571 should be used rather than directly modifying self.vertices 

2572 if possible. 

2573 

2574 Parameters 

2575 ------------ 

2576 matrix : (4, 4) float 

2577 Homogeneous transformation matrix 

2578 """ 

2579 # get c-order float64 matrix 

2580 matrix = np.asanyarray(matrix, order="C", dtype=float64) 

2581 

2582 # only support homogeneous transformations 

2583 if matrix.shape != (4, 4): 

2584 raise ValueError("Transformation matrix must be (4, 4)!") 

2585 

2586 # exit early if we've been passed an identity matrix 

2587 # np.allclose is surprisingly slow so do this test 

2588 elif util.allclose(matrix, _IDENTITY4, 1e-8): 

2589 return self 

2590 

2591 # new vertex positions 

2592 new_vertices = transformations.transform_points(self.vertices, matrix=matrix) 

2593 

2594 # check to see if the matrix has rotation 

2595 # rather than just translation 

2596 has_rotation = not util.allclose(matrix[:3, :3], _IDENTITY3, atol=1e-6) 

2597 

2598 # transform overridden center of mass 

2599 if "center_mass" in self._data: 

2600 center_mass = [self._data["center_mass"]] 

2601 self.center_mass = transformations.transform_points( 

2602 center_mass, 

2603 matrix, 

2604 )[0] 

2605 

2606 # preserve face normals if we have them stored 

2607 if has_rotation and "face_normals" in self._cache: 

2608 # transform face normals by rotation component 

2609 self._cache.cache["face_normals"] = util.unitize( 

2610 transformations.transform_points( 

2611 self.face_normals, matrix=matrix, translate=False 

2612 ) 

2613 ) 

2614 

2615 # preserve vertex normals if we have them stored 

2616 if has_rotation and "vertex_normals" in self._cache: 

2617 self._cache.cache["vertex_normals"] = util.unitize( 

2618 transformations.transform_points( 

2619 self.vertex_normals, matrix=matrix, translate=False 

2620 ) 

2621 ) 

2622 

2623 # if transformation flips winding of triangles 

2624 if has_rotation and transformations.flips_winding(matrix): 

2625 log.debug("transform flips winding") 

2626 # fliplr will make array non C contiguous 

2627 # which will cause hashes to be more 

2628 # expensive than necessary so wrap 

2629 self.faces = np.ascontiguousarray(np.fliplr(self.faces)) 

2630 

2631 # assign the new values 

2632 self.vertices = new_vertices 

2633 

2634 # preserve normals and topology in cache 

2635 # while dumping everything else 

2636 self._cache.clear( 

2637 exclude={ 

2638 "face_normals", # transformed by us 

2639 "vertex_normals", # also transformed by us 

2640 "face_adjacency", # topological 

2641 "face_adjacency_edges", 

2642 "face_adjacency_unshared", 

2643 "edges", 

2644 "edges_face", 

2645 "edges_sorted", 

2646 "edges_unique", 

2647 "edges_unique_idx", 

2648 "edges_unique_inverse", 

2649 "edges_sparse", 

2650 "body_count", 

2651 "faces_unique_edges", 

2652 "euler_number", 

2653 } 

2654 ) 

2655 # set the cache ID with the current hash value 

2656 self._cache.id_set() 

2657 return self 

2658 

2659 def voxelized(self, pitch: Floating | None, method: str = "subdivide", **kwargs): 

2660 """ 

2661 Return a VoxelGrid object representing the current mesh 

2662 discretized into voxels at the specified pitch 

2663 

2664 Parameters 

2665 ------------ 

2666 pitch : float 

2667 The edge length of a single voxel 

2668 method: implementation key. See `trimesh.voxel.creation.voxelizers` 

2669 **kwargs: additional kwargs passed to the specified implementation. 

2670 

2671 Returns 

2672 ---------- 

2673 voxelized : VoxelGrid object 

2674 Representing the current mesh 

2675 """ 

2676 from .voxel import creation 

2677 

2678 return creation.voxelize(mesh=self, pitch=pitch, method=method, **kwargs) 

2679 

2680 def simplify_quadric_decimation( 

2681 self, 

2682 percent: Floating | None = None, 

2683 face_count: Integer | None = None, 

2684 aggression: Integer | None = None, 

2685 ) -> "Trimesh": 

2686 """ 

2687 A thin wrapper around `pip install fast-simplification`. 

2688 

2689 Parameters 

2690 ----------- 

2691 percent 

2692 A number between 0.0 and 1.0 for how much 

2693 face_count 

2694 Target number of faces desired in the resulting mesh. 

2695 aggression 

2696 An integer between `0` and `10`, the scale being roughly 

2697 `0` is "slow and good" and `10` being "fast and bad." 

2698 

2699 Returns 

2700 --------- 

2701 simple : trimesh.Trimesh 

2702 Simplified version of mesh. 

2703 """ 

2704 from fast_simplification import simplify 

2705 

2706 # create keyword arguments as dict so we can filter out `None` 

2707 # values as the C wrapper as of writing is not happy with `None` 

2708 # and requires they be omitted from the constructor 

2709 kwargs = { 

2710 "target_count": face_count, 

2711 "target_reduction": percent, 

2712 "agg": aggression, 

2713 } 

2714 

2715 # todo : one could take the `return_collapses=True` array and use it to 

2716 # apply the same simplification to the visual info 

2717 vertices, faces = simplify( 

2718 points=self.vertices.view(np.ndarray), 

2719 triangles=self.faces.view(np.ndarray), 

2720 **{k: v for k, v in kwargs.items() if v is not None}, 

2721 ) 

2722 

2723 return Trimesh(vertices=vertices, faces=faces) 

2724 

2725 def outline(self, face_ids: NDArray[int64] | None = None, **kwargs) -> Path3D: 

2726 """ 

2727 Given a list of face indexes find the outline of those 

2728 faces and return it as a Path3D. 

2729 

2730 The outline is defined here as every edge which is only 

2731 included by a single triangle. 

2732 

2733 Note that this implies a non-watertight mesh as the 

2734 outline of a watertight mesh is an empty path. 

2735 

2736 Parameters 

2737 ------------ 

2738 face_ids : (n, ) int 

2739 Indices to compute the outline of. 

2740 If None, outline of full mesh will be computed. 

2741 **kwargs: passed to Path3D constructor 

2742 

2743 Returns 

2744 ---------- 

2745 path : Path3D 

2746 Curve in 3D of the outline 

2747 """ 

2748 from .path.exchange.misc import faces_to_path 

2749 

2750 return Path3D(**faces_to_path(self, face_ids, **kwargs)) 

2751 

2752 def projected(self, normal: ArrayLike, **kwargs) -> Path2D: 

2753 """ 

2754 Project a mesh onto a plane and then extract the 

2755 polygon that outlines the mesh projection on that 

2756 plane. 

2757 

2758 Parameters 

2759 ---------- 

2760 normal : (3,) float 

2761 Normal to extract flat pattern along 

2762 origin : None or (3,) float 

2763 Origin of plane to project mesh onto 

2764 ignore_sign : bool 

2765 Allow a projection from the normal vector in 

2766 either direction: this provides a substantial speedup 

2767 on watertight meshes where the direction is irrelevant 

2768 but if you have a triangle soup and want to discard 

2769 backfaces you should set this to False. 

2770 rpad : float 

2771 Proportion to pad polygons by before unioning 

2772 and then de-padding result by to avoid zero-width gaps. 

2773 apad : float 

2774 Absolute padding to pad polygons by before unioning 

2775 and then de-padding result by to avoid zero-width gaps. 

2776 tol_dot : float 

2777 Tolerance for discarding on-edge triangles. 

2778 precise : bool 

2779 Use the precise projection computation using shapely. 

2780 precise_eps : float 

2781 Tolerance for precise triangle checks. 

2782 

2783 Returns 

2784 ---------- 

2785 projected : trimesh.path.Path2D 

2786 Outline of source mesh 

2787 """ 

2788 from .exchange.load import load_path 

2789 from .path import Path2D 

2790 from .path.polygons import projected 

2791 

2792 projection = projected(mesh=self, normal=normal, **kwargs) 

2793 if projection is None: 

2794 return Path2D() 

2795 return load_path(projection) 

2796 

2797 @cache_decorator 

2798 def area(self) -> float64: 

2799 """ 

2800 Summed area of all triangles in the current mesh. 

2801 

2802 Returns 

2803 --------- 

2804 area : float 

2805 Surface area of mesh 

2806 """ 

2807 area = self.area_faces.sum() 

2808 return area 

2809 

2810 @cache_decorator 

2811 def area_faces(self) -> NDArray[float64]: 

2812 """ 

2813 The area of each face in the mesh. 

2814 

2815 Returns 

2816 --------- 

2817 area_faces : (n, ) float 

2818 Area of each face 

2819 """ 

2820 return triangles.area(crosses=self.triangles_cross) 

2821 

2822 @cache_decorator 

2823 def mass_properties(self) -> MassProperties: 

2824 """ 

2825 Returns the mass properties of the current mesh. 

2826 

2827 Assumes uniform density, and result is probably garbage if mesh 

2828 isn't watertight. 

2829 

2830 Returns 

2831 ---------- 

2832 properties : dict 

2833 With keys: 

2834 'volume' : in global units^3 

2835 'mass' : From specified density 

2836 'density' : Included again for convenience (same as kwarg density) 

2837 'inertia' : Taken at the center of mass and aligned with global 

2838 coordinate system 

2839 'center_mass' : Center of mass location, in global coordinate system 

2840 """ 

2841 # if the density or center of mass was overridden they will be put into data 

2842 density = self._data.data.get("density", None) 

2843 center_mass = self._data.data.get("center_mass", None) 

2844 return triangles.mass_properties( 

2845 triangles=self.triangles, 

2846 crosses=self.triangles_cross, 

2847 density=density, 

2848 center_mass=center_mass, 

2849 skip_inertia=False, 

2850 ) 

2851 

2852 def invert(self) -> Self: 

2853 """ 

2854 Invert the mesh in-place by reversing the winding of every 

2855 face and negating normals without dumping the cache. 

2856 

2857 Alters `self.faces` by reversing columns, and negating 

2858 `self.face_normals` and `self.vertex_normals`. 

2859 """ 

2860 with self._cache: 

2861 if "face_normals" in self._cache: 

2862 self.face_normals = self._cache["face_normals"] * -1.0 

2863 if "vertex_normals" in self._cache: 

2864 self.vertex_normals = self._cache["vertex_normals"] * -1.0 

2865 # fliplr makes array non-contiguous so cache checks slow 

2866 self.faces = np.ascontiguousarray(np.fliplr(self.faces)) 

2867 # save our normals 

2868 self._cache.clear(exclude=["face_normals", "vertex_normals"]) 

2869 

2870 return self 

2871 

2872 def scene(self, **kwargs) -> Scene: 

2873 """ 

2874 Returns a Scene object containing the current mesh. 

2875 

2876 Returns 

2877 --------- 

2878 scene : trimesh.scene.scene.Scene 

2879 Contains just the current mesh 

2880 """ 

2881 return Scene(self, **kwargs) 

2882 

2883 def show( 

2884 self, 

2885 viewer: ViewerType = None, 

2886 **kwargs, 

2887 ) -> Scene: 

2888 """ 

2889 Render the mesh in an opengl window. Requires pyglet. 

2890 

2891 Parameters 

2892 ------------ 

2893 viewer : ViewerType 

2894 What kind of viewer to use, such as 

2895 `gl` to open a pyglet window 

2896 `jupyter` for a jupyter notebook 

2897 `marimo'` for a marimo notebook 

2898 None for a "best guess" 

2899 smooth : bool 

2900 Run smooth shading on mesh or not, 

2901 large meshes will be slow 

2902 

2903 Returns 

2904 ----------- 

2905 scene : trimesh.scene.Scene 

2906 Scene with current mesh in it 

2907 """ 

2908 scene = self.scene() 

2909 return scene.show(viewer=viewer, **kwargs) 

2910 

2911 def submesh( 

2912 self, 

2913 faces_sequence: Sequence[ArrayLike], 

2914 only_watertight: bool = False, 

2915 repair: bool = False, 

2916 **kwargs, 

2917 ) -> "Trimesh | list[Trimesh]": 

2918 """ 

2919 Return a subset of the mesh. 

2920 

2921 Parameters 

2922 ------------ 

2923 faces_sequence : sequence (m, ) int 

2924 Face indices of mesh 

2925 only_watertight : bool 

2926 Only return submeshes which are watertight 

2927 repair 

2928 Try to repair the submesh if it is not watertight 

2929 append : bool 

2930 Return a single mesh which has the faces appended. 

2931 if this flag is set, only_watertight is ignored 

2932 

2933 Returns 

2934 --------- 

2935 submesh : Trimesh or (n,) Trimesh 

2936 Single mesh if `append` or list of submeshes 

2937 """ 

2938 return util.submesh( 

2939 mesh=self, 

2940 faces_sequence=faces_sequence, 

2941 only_watertight=only_watertight, 

2942 repair=repair, 

2943 **kwargs, 

2944 ) 

2945 

2946 @cache_decorator 

2947 def identifier(self) -> NDArray[float64]: 

2948 """ 

2949 Return a float vector which is unique to the mesh 

2950 and is robust to rotation and translation. 

2951 

2952 Returns 

2953 ----------- 

2954 identifier : (7,) float 

2955 Identifying properties of the current mesh 

2956 """ 

2957 return comparison.identifier_simple(self) 

2958 

2959 @cache_decorator 

2960 def identifier_hash(self) -> str: 

2961 """ 

2962 A hash of the rotation invariant identifier vector. 

2963 

2964 Returns 

2965 --------- 

2966 hashed : str 

2967 Hex string of the SHA256 hash from 

2968 the identifier vector at hand-tuned sigfigs. 

2969 """ 

2970 return comparison.identifier_hash(self.identifier) 

2971 

2972 def export( 

2973 self, 

2974 file_obj: Loadable = None, 

2975 file_type: str | None = None, 

2976 **kwargs, 

2977 ) -> dict | bytes | str: 

2978 """ 

2979 Export the current mesh to a file object. 

2980 If file_obj is a filename, file will be written there. 

2981 

2982 Supported formats are stl, off, ply, collada, json, 

2983 dict, glb, dict64, msgpack. 

2984 

2985 Parameters 

2986 ------------ 

2987 file_obj : open writeable file object 

2988 str, file name where to save the mesh 

2989 None, return the export blob 

2990 file_type : str 

2991 Which file type to export as, if `file_name` 

2992 is passed this is not required. 

2993 

2994 Returns 

2995 ---------- 

2996 exported : bytes or str 

2997 Result of exporter 

2998 """ 

2999 return export_mesh(mesh=self, file_obj=file_obj, file_type=file_type, **kwargs) 

3000 

3001 def to_dict(self) -> dict[str, str | list[list[float]] | list[list[int]]]: 

3002 """ 

3003 Return a dictionary representation of the current mesh 

3004 with keys that can be used as the kwargs for the 

3005 Trimesh constructor and matches the schema in: 

3006 `trimesh/resources/schema/primitive/trimesh.schema.json` 

3007 

3008 Returns 

3009 ---------- 

3010 result : dict 

3011 Matches schema and Trimesh constructor. 

3012 """ 

3013 return { 

3014 "kind": "trimesh", 

3015 "vertices": self.vertices.tolist(), 

3016 "faces": self.faces.tolist(), 

3017 } 

3018 

3019 def convex_decomposition(self, **kwargs) -> list["Trimesh"]: 

3020 """ 

3021 Compute an approximate convex decomposition of a mesh 

3022 using `pip install pyVHACD`. 

3023 

3024 Returns 

3025 ------- 

3026 meshes 

3027 List of convex meshes that approximate the original 

3028 **kwargs : VHACD keyword arguments 

3029 """ 

3030 return [ 

3031 Trimesh(**kwargs) 

3032 for kwargs in decomposition.convex_decomposition(self, **kwargs) 

3033 ] 

3034 

3035 def union( 

3036 self, 

3037 other: "Trimesh | Sequence[Trimesh]", 

3038 engine: BooleanEngineType = None, 

3039 check_volume: bool = True, 

3040 **kwargs, 

3041 ) -> "Trimesh": 

3042 """ 

3043 Boolean union between this mesh and other meshes. 

3044 

3045 Parameters 

3046 ------------ 

3047 other : Trimesh or (n, ) Trimesh 

3048 Other meshes to union 

3049 engine 

3050 Which backend to use, the default 

3051 recommendation is: `pip install manifold3d`. 

3052 check_volume 

3053 Raise an error if not all meshes are watertight 

3054 positive volumes. Advanced users may want to ignore 

3055 this check as it is expensive. 

3056 kwargs 

3057 Passed through to the `engine`. 

3058 

3059 Returns 

3060 --------- 

3061 union : trimesh.Trimesh 

3062 Union of self and other Trimesh objects 

3063 """ 

3064 return boolean.union( 

3065 meshes=util.chain(self, other), 

3066 engine=engine, 

3067 check_volume=check_volume, 

3068 **kwargs, 

3069 ) 

3070 

3071 def difference( 

3072 self, 

3073 other: "Trimesh | Sequence[Trimesh]", 

3074 engine: BooleanEngineType = None, 

3075 check_volume: bool = True, 

3076 **kwargs, 

3077 ) -> "Trimesh": 

3078 """ 

3079 Boolean difference between this mesh and other meshes. 

3080 

3081 Parameters 

3082 ------------ 

3083 other 

3084 One or more meshes to difference with the current mesh. 

3085 engine 

3086 Which backend to use, the default 

3087 recommendation is: `pip install manifold3d`. 

3088 check_volume 

3089 Raise an error if not all meshes are watertight 

3090 positive volumes. Advanced users may want to ignore 

3091 this check as it is expensive. 

3092 kwargs 

3093 Passed through to the `engine`. 

3094 

3095 Returns 

3096 --------- 

3097 difference : trimesh.Trimesh 

3098 Difference between self and other Trimesh objects 

3099 """ 

3100 return boolean.difference( 

3101 meshes=util.chain(self, other), 

3102 engine=engine, 

3103 check_volume=check_volume, 

3104 **kwargs, 

3105 ) 

3106 

3107 def intersection( 

3108 self, 

3109 other: "Trimesh | Sequence[Trimesh]", 

3110 engine: BooleanEngineType = None, 

3111 check_volume: bool = True, 

3112 **kwargs, 

3113 ) -> "Trimesh": 

3114 """ 

3115 Boolean intersection between this mesh and other meshes. 

3116 

3117 Parameters 

3118 ------------ 

3119 other : trimesh.Trimesh, or list of trimesh.Trimesh objects 

3120 Meshes to calculate intersections with 

3121 engine 

3122 Which backend to use, the default 

3123 recommendation is: `pip install manifold3d`. 

3124 check_volume 

3125 Raise an error if not all meshes are watertight 

3126 positive volumes. Advanced users may want to ignore 

3127 this check as it is expensive. 

3128 kwargs 

3129 Passed through to the `engine`. 

3130 

3131 Returns 

3132 --------- 

3133 intersection : trimesh.Trimesh 

3134 Mesh of the volume contained by all passed meshes 

3135 """ 

3136 return boolean.intersection( 

3137 meshes=util.chain(self, other), 

3138 engine=engine, 

3139 check_volume=check_volume, 

3140 **kwargs, 

3141 ) 

3142 

3143 def contains(self, points: ArrayLike) -> NDArray[np.bool_]: 

3144 """ 

3145 Given an array of points determine whether or not they 

3146 are inside the mesh. This raises an error if called on a 

3147 non-watertight mesh. 

3148 

3149 Parameters 

3150 ------------ 

3151 points : (n, 3) float 

3152 Points in cartesian space 

3153 

3154 Returns 

3155 --------- 

3156 contains : (n, ) bool 

3157 Whether or not each point is inside the mesh 

3158 """ 

3159 return self.ray.contains_points(points) 

3160 

3161 @cache_decorator 

3162 def face_angles(self) -> NDArray[float64]: 

3163 """ 

3164 Returns the angle at each vertex of a face. 

3165 

3166 Returns 

3167 -------- 

3168 angles : (len(self.faces), 3) float 

3169 Angle at each vertex of a face 

3170 """ 

3171 return triangles.angles(self.triangles) 

3172 

3173 @cache_decorator 

3174 def face_angles_sparse(self) -> coo_matrix: 

3175 """ 

3176 A sparse matrix representation of the face angles. 

3177 

3178 Returns 

3179 ---------- 

3180 sparse : scipy.sparse.coo_matrix 

3181 Float sparse matrix with with shape: 

3182 (len(self.vertices), len(self.faces)) 

3183 """ 

3184 angles = curvature.face_angles_sparse(self) 

3185 return angles 

3186 

3187 @cache_decorator 

3188 def vertex_defects(self) -> NDArray[float64]: 

3189 """ 

3190 Return the vertex defects, or (2*pi) minus the sum of the angles 

3191 of every face that includes that vertex. 

3192 

3193 If a vertex is only included by coplanar triangles, this 

3194 will be zero. For convex regions this is positive, and 

3195 concave negative. 

3196 

3197 Returns 

3198 -------- 

3199 vertex_defect : (len(self.vertices), ) float 

3200 Vertex defect at the every vertex 

3201 """ 

3202 defects = curvature.vertex_defects(self) 

3203 return defects 

3204 

3205 @cache_decorator 

3206 def vertex_degree(self) -> NDArray[int64]: 

3207 """ 

3208 Return the number of faces each vertex is included in. 

3209 

3210 Returns 

3211 ---------- 

3212 degree : (len(self.vertices), ) int 

3213 Number of faces each vertex is included in 

3214 """ 

3215 # get degree through sparse matrix 

3216 degree = np.array(self.faces_sparse.sum(axis=1)).flatten() 

3217 return degree 

3218 

3219 @cache_decorator 

3220 def face_adjacency_tree(self) -> Index: 

3221 """ 

3222 An R-tree of face adjacencies. 

3223 

3224 Returns 

3225 -------- 

3226 tree 

3227 Where each edge in self.face_adjacency has a 

3228 rectangular cell 

3229 """ 

3230 # the (n,6) interleaved bounding box for every line segment 

3231 return util.bounds_tree( 

3232 np.column_stack( 

3233 ( 

3234 self.vertices[self.face_adjacency_edges].min(axis=1), 

3235 self.vertices[self.face_adjacency_edges].max(axis=1), 

3236 ) 

3237 ) 

3238 ) 

3239 

3240 def copy(self, include_cache: bool = False, include_visual: bool = True) -> "Trimesh": 

3241 """ 

3242 Safely return a copy of the current mesh. 

3243 

3244 By default, copied meshes will have emptied cache 

3245 to avoid memory issues and so may be slow on initial 

3246 operations until caches are regenerated. 

3247 

3248 Current object will *never* have its cache cleared. 

3249 

3250 Parameters 

3251 ------------ 

3252 include_cache : bool 

3253 If True, will shallow copy cached data to new mesh 

3254 include_visual : bool 

3255 If True, will copy visual information 

3256 

3257 Returns 

3258 --------- 

3259 copied : trimesh.Trimesh 

3260 Copy of current mesh 

3261 """ 

3262 # start with an empty mesh 

3263 copied = Trimesh() 

3264 # always deepcopy vertex and face data 

3265 copied._data.data = deepcopy(self._data.data) 

3266 

3267 if include_visual: 

3268 # copy visual information 

3269 copied.visual = self.visual.copy() 

3270 

3271 copied.vertex_attributes.update( 

3272 {k: deepcopy(v) for k, v in self.vertex_attributes.items()} 

3273 ) 

3274 copied.face_attributes.update( 

3275 {k: deepcopy(v) for k, v in self.face_attributes.items()} 

3276 ) 

3277 

3278 # get metadata 

3279 copied.metadata = deepcopy(self.metadata) 

3280 

3281 # make sure cache ID is set initially 

3282 copied._cache.verify() 

3283 

3284 if include_cache: 

3285 # shallow copy cached items into the new cache 

3286 # since the data didn't change here when the 

3287 # data in the new mesh is changed these items 

3288 # will be dumped in the new mesh but preserved 

3289 # in the original mesh 

3290 copied._cache.cache.update(self._cache.cache) 

3291 

3292 return copied 

3293 

3294 def __deepcopy__(self, *args) -> "Trimesh": 

3295 # interpret deep copy as "get rid of cached data" 

3296 return self.copy(include_cache=False) 

3297 

3298 def __copy__(self, *args) -> "Trimesh": 

3299 # interpret shallow copy as "keep cached data" 

3300 return self.copy(include_cache=True) 

3301 

3302 def eval_cached(self, statement: str, *args) -> Any: 

3303 """ 

3304 DEPRECATED: call `eval` directly instead. 

3305 

3306 Evaluate a statement and cache the result before returning. 

3307 

3308 Parameters 

3309 ------------ 

3310 statement : str 

3311 Statement of valid python code 

3312 *args : list 

3313 Available inside statement as args[0], etc 

3314 

3315 Returns 

3316 ----------- 

3317 result : result of running eval on statement with args 

3318 

3319 Examples 

3320 ----------- 

3321 r = mesh.eval_cached('np.dot(self.vertices, args[0])', [0, 0, 1]) 

3322 """ 

3323 import warnings 

3324 

3325 warnings.warn( 

3326 "`Trimesh.eval_cached` is deprecated " 

3327 + "and will be removed in a future release. " 

3328 + "call `eval` directly if you need this behavior.", 

3329 category=DeprecationWarning, 

3330 stacklevel=2, 

3331 ) 

3332 

3333 # store this by the combined hash of statement and args 

3334 hashable = [hash(statement)] 

3335 hashable.extend(hash(a) for a in args) 

3336 

3337 key = f"eval_cached_{hash(tuple(hashable))}" 

3338 

3339 if key in self._cache: 

3340 return self._cache[key] 

3341 

3342 result = eval(statement) 

3343 self._cache[key] = result 

3344 return result 

3345 

3346 def __add__(self, other: "Trimesh") -> "Trimesh": 

3347 """ 

3348 Concatenate the mesh with another mesh. 

3349 

3350 Parameters 

3351 ------------ 

3352 other : trimesh.Trimesh object 

3353 Mesh to be concatenated with self 

3354 

3355 Returns 

3356 ---------- 

3357 concat : trimesh.Trimesh 

3358 Mesh object of combined result 

3359 """ 

3360 concat = util.concatenate(self, other) 

3361 return concat