Coverage for trimesh/base.py: 93%

810 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-07-31 23:55 +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 Seed, 

57 Self, 

58 Sequence, 

59 ViewerType, 

60) 

61from .visual import ColorVisuals, TextureVisuals, create_visual 

62 

63try: 

64 from scipy.sparse import coo_matrix 

65 from scipy.spatial import cKDTree 

66except BaseException as E: 

67 cKDTree = ExceptionWrapper(E) 

68 coo_matrix = ExceptionWrapper(E) 

69try: 

70 from networkx import Graph 

71except BaseException as E: 

72 Graph = ExceptionWrapper(E) 

73 

74try: 

75 from PIL import Image 

76except BaseException as E: 

77 Image = ExceptionWrapper(E) 

78 

79try: 

80 from rtree.index import Index 

81except BaseException as E: 

82 Index = ExceptionWrapper(E) 

83 

84try: 

85 from .path import Path2D, Path3D 

86except BaseException as E: 

87 Path2D = ExceptionWrapper(E) 

88 Path3D = ExceptionWrapper(E) 

89 

90# save immutable identity matrices for checks 

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

92_IDENTITY3.flags.writeable = False 

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

94_IDENTITY4.flags.writeable = False 

95 

96 

97class Trimesh(Geometry3D): 

98 def __init__( 

99 self, 

100 vertices: ArrayLike | None = None, 

101 faces: ArrayLike | None = None, 

102 face_normals: ArrayLike | None = None, 

103 vertex_normals: ArrayLike | None = None, 

104 face_colors: ArrayLike | None = None, 

105 vertex_colors: ArrayLike | None = None, 

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

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

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

109 process: bool = True, 

110 validate: bool = False, 

111 merge_tex: bool | None = None, 

112 merge_norm: bool | None = None, 

113 use_embree: bool = True, 

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

115 visual: ColorVisuals | TextureVisuals | None = None, 

116 **kwargs, 

117 ) -> None: 

118 """ 

119 A Trimesh object contains a triangular 3D mesh. 

120 

121 Parameters 

122 ------------ 

123 vertices : (n, 3) float 

124 Array of vertex locations 

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

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

127 face_normals : (m, 3) float 

128 Array of normal vectors corresponding to faces 

129 vertex_normals : (n, 3) float 

130 Array of normal vectors for vertices 

131 face_colors : (n, 3|4) uint8 

132 Array of colors for faces 

133 vertex_colors : (n, 3|4) uint8 

134 Array of colors for vertices 

135 face_attributes : dict 

136 Attributes corresponding to faces 

137 vertex_attributes : dict 

138 Attributes corresponding to vertices 

139 metadata : dict 

140 Any metadata about the mesh 

141 process : bool 

142 if True, Nan and Inf values will be removed 

143 immediately and vertices will be merged 

144 validate : bool 

145 If True, degenerate and duplicate faces will be 

146 removed immediately, and some functions will alter 

147 the mesh to ensure consistent results. 

148 merge_tex : bool 

149 If True textured meshes with UV coordinates will 

150 have vertices merged regardless of UV coordinates 

151 merge_norm : bool 

152 If True, meshes with vertex normals will have 

153 vertices merged ignoring different normals 

154 use_embree : bool 

155 If True try to use pyembree raytracer. 

156 If pyembree is not available it will automatically fall 

157 back to a much slower rtree/numpy implementation 

158 initial_cache : dict 

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

160 things were calculated before creating the mesh object. 

161 visual : ColorVisuals or TextureVisuals 

162 Assigned to self.visual 

163 """ 

164 

165 # self._data stores information about the mesh which 

166 # CANNOT be regenerated. 

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

168 # face information 

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

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

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

172 self._data = DataStore() 

173 

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

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

176 # In order to maintain consistency 

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

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

179 if initial_cache is not None: 

180 self._cache.update(initial_cache) 

181 

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

183 

184 # (n, 3) float array of vertices 

185 self.vertices = vertices 

186 

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

188 self.faces = faces 

189 

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

191 # be updated when an update_faces call is made 

192 self.face_attributes = {} 

193 self.vertex_attributes = {} 

194 

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

196 if visual is None: 

197 self.visual = create_visual( 

198 face_colors=face_colors, vertex_colors=vertex_colors, mesh=self 

199 ) 

200 else: 

201 self.visual = visual 

202 

203 # if we've been passed a visual object 

204 if vertex_colors is not None: 

205 self.vertex_attributes["color"] = vertex_colors 

206 if face_colors is not None: 

207 self.face_attributes["color"] = face_colors 

208 

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

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

211 # to avoid a substantial number of cross products 

212 if face_normals is not None: 

213 self.face_normals = face_normals 

214 

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

216 if vertex_normals is not None: 

217 self.vertex_normals = vertex_normals 

218 

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

220 # if you have pyembree installed you should use it 

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

222 if ray.has_embree and use_embree: 

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

224 else: 

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

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

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

228 # and is cached for subsequent queries 

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

230 

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

232 self.permutate = permutate.Permutator(self) 

233 

234 # convenience class for nearest point queries 

235 self.nearest = proximity.ProximityQuery(self) 

236 

237 # update the mesh metadata with passed metadata 

238 self.metadata = {} 

239 if isinstance(metadata, dict): 

240 self.metadata.update(metadata) 

241 elif metadata is not None: 

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

243 

244 # use update to copy items 

245 if face_attributes is not None: 

246 self.face_attributes.update(face_attributes) 

247 if vertex_attributes is not None: 

248 self.vertex_attributes.update(vertex_attributes) 

249 

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

251 # if validate, will remove degenerate and duplicate faces 

252 if process or validate: 

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

254 

255 def process( 

256 self, 

257 validate: bool = False, 

258 merge_tex: bool | None = None, 

259 merge_norm: bool | None = None, 

260 ) -> Self: 

261 """ 

262 Do processing to make a mesh useful. 

263 

264 Does this by: 

265 1) removing NaN and Inf values 

266 2) merging duplicate vertices 

267 If validate: 

268 3) Remove triangles which have one edge 

269 of their 2D oriented bounding box 

270 shorter than tol.merge 

271 4) remove duplicated triangles 

272 5) Attempt to ensure triangles are consistently wound 

273 and normals face outwards. 

274 

275 Parameters 

276 ------------ 

277 validate : bool 

278 Remove degenerate and duplicate faces. 

279 merge_tex : bool 

280 If True textured meshes with UV coordinates will 

281 have vertices merged regardless of UV coordinates 

282 merge_norm : bool 

283 If True, meshes with vertex normals will have 

284 vertices merged ignoring different normals 

285 

286 Returns 

287 ------------ 

288 self: trimesh.Trimesh 

289 Current mesh 

290 """ 

291 # if there are no vertices or faces exit early 

292 if self.is_empty: 

293 return self 

294 

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

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

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

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

299 if validate: 

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

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

302 self.update_faces(mask) 

303 self.fix_normals() 

304 

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

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

307 with self._cache: 

308 self.remove_infinite_values() 

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

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

311 

312 self.metadata["processed"] = True 

313 return self 

314 

315 @property 

316 def mutable(self) -> bool: 

317 """ 

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

319 

320 Returns 

321 ------------- 

322 mutable 

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

324 """ 

325 return self._data.mutable 

326 

327 @mutable.setter 

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

329 """ 

330 Set the mutability of the current mesh. 

331 

332 Parameters 

333 ---------- 

334 value 

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

336 """ 

337 self._data.mutable = value 

338 

339 @property 

340 def faces(self) -> TrackedArray: 

341 """ 

342 The faces of the mesh. 

343 

344 This is regarded as core information which cannot be 

345 regenerated from cache and as such is stored in 

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

347 clears cached values of the mesh altered. 

348 

349 Returns 

350 ---------- 

351 faces : (n, 3) int64 

352 References for `self.vertices` for triangles. 

353 """ 

354 return self._data["faces"] 

355 

356 @faces.setter 

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

358 """ 

359 Set the vertex indexes that make up triangular faces. 

360 

361 Parameters 

362 -------------- 

363 values : (n, 3) int64 

364 Indexes of self.vertices 

365 """ 

366 if values is None: 

367 # if passed none store an empty array 

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

369 else: 

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

371 

372 # automatically triangulate quad faces 

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

374 log.info("triangulating faces") 

375 values = geometry.triangulate_quads(values) 

376 

377 self._data["faces"] = values 

378 

379 @cache_decorator 

380 def faces_sparse(self) -> coo_matrix: 

381 """ 

382 A sparse matrix representation of the faces. 

383 

384 Returns 

385 ---------- 

386 sparse : scipy.sparse.coo_matrix 

387 Has properties: 

388 dtype : bool 

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

390 """ 

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

392 

393 @property 

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

395 """ 

396 Return the unit normal vector for each face. 

397 

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

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

400 

401 Returns 

402 ----------- 

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

404 Normal vectors of each face 

405 """ 

406 # check shape of cached normals 

407 cached = self._cache["face_normals"] 

408 # get faces from datastore 

409 if "faces" in self._data: 

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

411 else: 

412 faces = None 

413 

414 # if we have no faces exit early 

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

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

417 

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

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

420 return cached 

421 

422 # use cached triangle cross products to generate normals 

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

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

425 # a cross product below machine epsilon 

426 normals, valid = triangles.normals( 

427 triangles=self.triangles, crosses=self.triangles_cross 

428 ) 

429 

430 # if all triangles are valid shape is correct 

431 if valid.all(): 

432 # put calculated face normals into cache manually 

433 self._cache["face_normals"] = normals 

434 return normals 

435 

436 # make a padded list of normals for correct shape 

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

438 padded[valid] = normals 

439 

440 # put calculated face normals into cache manually 

441 self._cache["face_normals"] = padded 

442 

443 return padded 

444 

445 @face_normals.setter 

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

447 """ 

448 Assign values to face normals. 

449 

450 Parameters 

451 ------------- 

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

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

454 """ 

455 # if nothing passed exit 

456 if values is None: 

457 return 

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

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

460 # face normals need to correspond to faces 

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

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

463 return 

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

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

466 ptp = np.ptp(values) 

467 if not np.isfinite(ptp): 

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

469 return 

470 if ptp < tol.merge: 

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

472 return 

473 

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

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

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

477 compare[valid] = check 

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

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

480 return 

481 

482 # otherwise store face normals 

483 self._cache["face_normals"] = values 

484 

485 @property 

486 def vertices(self) -> TrackedArray: 

487 """ 

488 The vertices of the mesh. 

489 

490 This is regarded as core information which cannot be 

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

492 which tracks the array for changes and clears cached 

493 values of the mesh if this is altered. 

494 

495 Returns 

496 ---------- 

497 vertices : (n, 3) float 

498 Points in cartesian space referenced by self.faces 

499 """ 

500 # get vertices if already stored 

501 return self._data["vertices"] 

502 

503 @vertices.setter 

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

505 """ 

506 Assign vertex values to the mesh. 

507 

508 Parameters 

509 -------------- 

510 values : (n, 3) float 

511 Points in space 

512 """ 

513 if values is None: 

514 # remove any stored data and store an empty array 

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

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

517 

518 @cache_decorator 

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

520 """ 

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

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

523 normals and vertices before returning them. If there are 

524 no vertex normals defined or a shape mismatch we calculate 

525 the vertex normals from the mean normals of the faces the 

526 vertex is used in. 

527 

528 Returns 

529 ---------- 

530 vertex_normals : (n, 3) float 

531 Represents the surface normal at each vertex. 

532 Where n == len(self.vertices) 

533 """ 

534 # make sure we have faces_sparse 

535 return geometry.weighted_vertex_normals( 

536 vertex_count=len(self.vertices), 

537 faces=self.faces, 

538 face_normals=self.face_normals, 

539 face_angles=self.face_angles, 

540 ) 

541 

542 @vertex_normals.setter 

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

544 """ 

545 Assign values to vertex normals. 

546 

547 Parameters 

548 ------------- 

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

550 Unit normal vectors for each vertex 

551 """ 

552 if values is not None: 

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

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

555 # check to see if they assigned all zeros 

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

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

558 self._cache["vertex_normals"] = values 

559 

560 @cache_decorator 

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

562 """ 

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

564 

565 Returns 

566 ---------- 

567 vertex_faces : (n,m) int 

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

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

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

571 """ 

572 vertex_faces = geometry.vertex_face_indices( 

573 vertex_count=len(self.vertices), 

574 faces=self.faces, 

575 faces_sparse=self.faces_sparse, 

576 ) 

577 return vertex_faces 

578 

579 @cache_decorator 

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

581 """ 

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

583 

584 Returns 

585 ----------- 

586 bounds : (2, 3) float or None 

587 Bounding box with [min, max] coordinates 

588 If mesh is empty will return None 

589 """ 

590 # return bounds including ONLY referenced vertices 

591 in_mesh = self.vertices[self.referenced_vertices] 

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

593 if len(in_mesh) == 0: 

594 return None 

595 # get mesh bounds with min and max 

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

597 

598 @cache_decorator 

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

600 """ 

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

602 bounding box of the mesh. 

603 

604 Returns 

605 ----------- 

606 extents : (3, ) float or None 

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

608 If mesh is empty returns None 

609 """ 

610 # if mesh is empty return None 

611 if self.bounds is None: 

612 return None 

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

614 

615 return extents 

616 

617 @cache_decorator 

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

619 """ 

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

621 centroids weighted by the area of each triangle. 

622 

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

624 unlike self.center_mass 

625 

626 Returns 

627 ---------- 

628 centroid : (3, ) float 

629 The average vertex weighted by face area 

630 """ 

631 

632 # use the centroid of each triangle weighted by 

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

634 try: 

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

636 except BaseException: 

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

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

639 return centroid 

640 

641 @property 

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

643 """ 

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

645 

646 Returns 

647 ----------- 

648 center_mass : (3, ) float 

649 Volumetric center of mass of the mesh. 

650 """ 

651 return self.mass_properties.center_mass 

652 

653 @center_mass.setter 

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

655 """ 

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

657 

658 Parameters 

659 ----------- 

660 center_mass : (3, ) float 

661 Volumetric center of mass of the mesh. 

662 """ 

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

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

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

666 self._data["center_mass"] = value 

667 self._cache.delete("mass_properties") 

668 

669 @property 

670 def density(self) -> float: 

671 """ 

672 The density of the mesh used in inertia calculations. 

673 

674 Returns 

675 ----------- 

676 density 

677 The density of the primitive. 

678 """ 

679 return float(self.mass_properties.density) 

680 

681 @density.setter 

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

683 """ 

684 Set the density of the primitive. 

685 

686 Parameters 

687 ------------- 

688 density 

689 Specify the density of the primitive to be 

690 used in inertia calculations. 

691 """ 

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

693 self._cache.delete("mass_properties") 

694 

695 @property 

696 def volume(self) -> float64: 

697 """ 

698 Volume of the current mesh calculated using a surface 

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

700 garbage. 

701 

702 Returns 

703 --------- 

704 volume : float 

705 Volume of the current mesh 

706 """ 

707 return self.mass_properties.volume 

708 

709 @property 

710 def mass(self) -> float64: 

711 """ 

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

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

714 

715 Returns 

716 --------- 

717 mass : float 

718 Mass of the current mesh 

719 """ 

720 return self.mass_properties.mass 

721 

722 @property 

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

724 """ 

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

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

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

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

729 other frame including the origin call: 

730 `mesh.moment_inertia_frame` 

731 

732 Returns 

733 --------- 

734 inertia : (3, 3) float 

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

736 mass and aligned with the cartesian axis. 

737 """ 

738 return self.mass_properties.inertia 

739 

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

741 """ 

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

743 an arbitrary frame, versus with respect to the center 

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

745 

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

747 this will give the moment at the origin. 

748 

749 Uses the parallel axis theorum to move the center mass 

750 tensor to this arbitrary frame. 

751 

752 Parameters 

753 ------------ 

754 transform : (4, 4) float 

755 Homogeneous transformation matrix. 

756 

757 Returns 

758 ------------- 

759 inertia : (3, 3) 

760 Moment of inertia in the requested frame. 

761 """ 

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

763 props = self.mass_properties 

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

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

766 # center of mass 

767 offset = np.eye(4) 

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

769 

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

771 return inertia.transform_inertia( 

772 inertia_tensor=props["inertia"], 

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

774 mass=props["mass"], 

775 parallel_axis=True, 

776 ) 

777 

778 @cache_decorator 

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

780 """ 

781 Return the principal components of inertia 

782 

783 Ordering corresponds to mesh.principal_inertia_vectors 

784 

785 Returns 

786 ---------- 

787 components : (3, ) float 

788 Principal components of inertia 

789 """ 

790 # both components and vectors from inertia matrix 

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

792 # store vectors in cache for later 

793 self._cache["principal_inertia_vectors"] = vectors 

794 

795 return components 

796 

797 @property 

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

799 """ 

800 Return the principal axis of inertia as unit vectors. 

801 The order corresponds to `mesh.principal_inertia_components`. 

802 

803 Returns 

804 ---------- 

805 vectors : (3, 3) float 

806 Three vectors pointing along the 

807 principal axis of inertia directions 

808 """ 

809 _ = self.principal_inertia_components 

810 return self._cache["principal_inertia_vectors"] 

811 

812 @cache_decorator 

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

814 """ 

815 A transform which moves the current mesh so the principal 

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

817 at the origin. 

818 

819 Returns 

820 ---------- 

821 transform : (4, 4) float 

822 Homogeneous transformation matrix 

823 """ 

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

825 vectors = self.principal_inertia_vectors[order] 

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

827 

828 transform = np.eye(4) 

829 transform[:3, :3] = vectors 

830 transform = transformations.transform_around( 

831 matrix=transform, point=self.centroid 

832 ) 

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

834 

835 return transform 

836 

837 @cache_decorator 

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

839 """ 

840 Check whether a mesh has rotational symmetry around 

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

842 

843 Returns 

844 ----------- 

845 symmetry : None, 'radial', 'spherical' 

846 What kind of symmetry does the mesh have. 

847 """ 

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

849 self._cache["symmetry_axis"] = axis 

850 self._cache["symmetry_section"] = section 

851 return symmetry 

852 

853 @property 

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

855 """ 

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

857 

858 Returns 

859 ------------ 

860 axis : (3, ) float 

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

862 """ 

863 if self.symmetry is None: 

864 return None 

865 return self._cache["symmetry_axis"] 

866 

867 @property 

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

869 """ 

870 If a mesh has rotational symmetry return the two 

871 vectors which make up a section coordinate frame. 

872 

873 Returns 

874 ---------- 

875 section : (2, 3) float 

876 Vectors to take a section along 

877 """ 

878 if self.symmetry is None: 

879 return None 

880 return self._cache["symmetry_section"] 

881 

882 @cache_decorator 

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

884 """ 

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

886 

887 Returns 

888 --------- 

889 triangles : (n, 3, 3) float 

890 Points of triangle vertices 

891 """ 

892 # use of advanced indexing on our tracked arrays will 

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

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

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

896 

897 @cache_decorator 

898 def triangles_tree(self) -> Index: 

899 """ 

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

901 

902 Returns 

903 ---------- 

904 tree : rtree.index 

905 Each triangle in self.faces has a rectangular cell 

906 """ 

907 return triangles.bounds_tree(self.triangles) 

908 

909 @cache_decorator 

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

911 """ 

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

913 

914 Returns 

915 --------- 

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

917 Center of each triangular face 

918 """ 

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

920 

921 @cache_decorator 

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

923 """ 

924 The cross product of two edges of each triangle. 

925 

926 Returns 

927 --------- 

928 crosses : (n, 3) float 

929 Cross product of each triangle 

930 """ 

931 crosses = triangles.cross(self.triangles) 

932 return crosses 

933 

934 @cache_decorator 

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

936 """ 

937 Edges of the mesh (derived from faces). 

938 

939 Returns 

940 --------- 

941 edges : (n, 2) int 

942 List of vertex indices making up edges 

943 """ 

944 edges, index = geometry.faces_to_edges( 

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

946 ) 

947 self._cache["edges_face"] = index 

948 return edges 

949 

950 @cache_decorator 

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

952 """ 

953 Which face does each edge belong to. 

954 

955 Returns 

956 --------- 

957 edges_face : (n, ) int 

958 Index of self.faces 

959 """ 

960 _ = self.edges 

961 return self._cache["edges_face"] 

962 

963 @cache_decorator 

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

965 """ 

966 The unique edges of the mesh. 

967 

968 Returns 

969 ---------- 

970 edges_unique : (n, 2) int 

971 Vertex indices for unique edges 

972 """ 

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

974 edges_unique = self.edges_sorted[unique] 

975 # edges_unique will be added automatically by the decorator 

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

977 self._cache["edges_unique_idx"] = unique 

978 self._cache["edges_unique_inverse"] = inverse 

979 return edges_unique 

980 

981 @cache_decorator 

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

983 """ 

984 How long is each unique edge. 

985 

986 Returns 

987 ---------- 

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

989 Length of each unique edge 

990 """ 

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

992 length = util.row_norm(vector) 

993 return length 

994 

995 @cache_decorator 

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

997 """ 

998 Return the inverse required to reproduce 

999 self.edges_sorted from self.edges_unique. 

1000 

1001 Useful for referencing edge properties: 

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

1003 

1004 Returns 

1005 ---------- 

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

1007 Indexes of self.edges_unique 

1008 """ 

1009 _ = self.edges_unique 

1010 return self._cache["edges_unique_inverse"] 

1011 

1012 @cache_decorator 

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

1014 """ 

1015 Edges sorted along axis 1 

1016 

1017 Returns 

1018 ---------- 

1019 edges_sorted : (n, 2) 

1020 Same as self.edges but sorted along axis 1 

1021 """ 

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

1023 return edges_sorted 

1024 

1025 @cache_decorator 

1026 def edges_sorted_tree(self) -> cKDTree: 

1027 """ 

1028 A KDTree for mapping edges back to edge index. 

1029 

1030 Returns 

1031 ------------ 

1032 tree : scipy.spatial.cKDTree 

1033 Tree when queried with edges will return 

1034 their index in mesh.edges_sorted 

1035 """ 

1036 return cKDTree(self.edges_sorted) 

1037 

1038 @cache_decorator 

1039 def edges_sparse(self) -> coo_matrix: 

1040 """ 

1041 Edges in sparse bool COO graph format where connected 

1042 vertices are True. 

1043 

1044 Returns 

1045 ---------- 

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

1047 Sparse graph in COO format 

1048 """ 

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

1050 return sparse 

1051 

1052 @cache_decorator 

1053 def body_count(self) -> int: 

1054 """ 

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

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

1057 which is calculated from FACE rather than vertex adjacency. 

1058 

1059 Returns 

1060 ----------- 

1061 count : int 

1062 Number of connected vertex groups 

1063 """ 

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

1065 count, labels = graph.csgraph.connected_components( 

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

1067 ) 

1068 self._cache["vertices_component_label"] = labels 

1069 return count 

1070 

1071 @cache_decorator 

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

1073 """ 

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

1075 that face. 

1076 

1077 Returns 

1078 --------- 

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

1080 Indexes of self.edges_unique that 

1081 construct self.faces 

1082 

1083 Examples 

1084 --------- 

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

1086 Out[0]: 

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

1088 [ 6946, 1727, 24225]]) 

1089 

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

1091 Out[1]: 

1092 array([[[ 1, 6946], 

1093 [ 6946, 24224], 

1094 [ 1, 24224]], 

1095 [[ 1727, 6946], 

1096 [ 1727, 24225], 

1097 [ 6946, 24225]]]) 

1098 """ 

1099 # make sure we have populated unique edges 

1100 _ = self.edges_unique 

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

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

1103 return result 

1104 

1105 @cache_decorator 

1106 def euler_number(self) -> int: 

1107 """ 

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

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

1110 remove_unreferenced_vertices 

1111 

1112 Returns 

1113 ---------- 

1114 euler_number : int 

1115 Topological invariant 

1116 """ 

1117 return int( 

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

1119 ) 

1120 

1121 @cache_decorator 

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

1123 """ 

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

1125 

1126 Returns 

1127 ------------- 

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

1129 Which vertices are referenced by a face 

1130 """ 

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

1132 referenced[self.faces] = True 

1133 return referenced 

1134 

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

1136 """ 

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

1138 

1139 Parameters 

1140 ------------ 

1141 desired : string 

1142 Units to convert to (eg 'inches') 

1143 guess : boolean 

1144 If self.units are not defined should we 

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

1146 

1147 Returns 

1148 ------------ 

1149 self: trimesh.Trimesh 

1150 Current mesh 

1151 """ 

1152 units._convert_units(self, desired, guess) 

1153 return self 

1154 

1155 def merge_vertices( 

1156 self, 

1157 merge_tex: bool | None = None, 

1158 merge_norm: bool | None = None, 

1159 digits_vertex: Integer | None = None, 

1160 digits_norm: Integer | None = None, 

1161 digits_uv: Integer | None = None, 

1162 ) -> None: 

1163 """ 

1164 Removes duplicate vertices grouped by position and 

1165 optionally texture coordinate and normal. 

1166 

1167 Parameters 

1168 ------------- 

1169 merge_tex : bool 

1170 If True textured meshes with UV coordinates will 

1171 have vertices merged regardless of UV coordinates 

1172 merge_norm : bool 

1173 If True, meshes with vertex normals will have 

1174 vertices merged ignoring different normals 

1175 digits_vertex : None or int 

1176 Number of digits to consider for vertex position 

1177 digits_norm : int 

1178 Number of digits to consider for unit normals 

1179 digits_uv : int 

1180 Number of digits to consider for UV coordinates 

1181 """ 

1182 grouping.merge_vertices( 

1183 mesh=self, 

1184 merge_tex=merge_tex, 

1185 merge_norm=merge_norm, 

1186 digits_vertex=digits_vertex, 

1187 digits_norm=digits_norm, 

1188 digits_uv=digits_uv, 

1189 ) 

1190 

1191 def update_vertices( 

1192 self, 

1193 mask: ArrayLike, 

1194 inverse: ArrayLike | None = None, 

1195 ) -> None: 

1196 """ 

1197 Update vertices with a mask. 

1198 

1199 Parameters 

1200 ------------ 

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

1202 Array of which vertices to keep 

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

1204 Array to reconstruct vertex references 

1205 such as output by np.unique 

1206 """ 

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

1208 if self.is_empty: 

1209 return 

1210 

1211 # make sure mask is a numpy array 

1212 mask = np.asanyarray(mask) 

1213 

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

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

1216 return 

1217 

1218 # create the inverse mask if not passed 

1219 if inverse is None: 

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

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

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

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

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

1225 else: 

1226 inverse = None 

1227 

1228 # re-index faces from inverse 

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

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

1231 

1232 # update the visual object with our mask 

1233 self.visual.update_vertices(mask) 

1234 # get the normals from cache before dumping 

1235 cached_normals = self._cache["vertex_normals"] 

1236 

1237 # apply to face_attributes 

1238 count = len(self.vertices) 

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

1240 try: 

1241 # covers un-len'd objects as well 

1242 if len(value) != count: 

1243 raise TypeError() 

1244 except TypeError: 

1245 continue 

1246 # apply the mask to the attribute 

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

1248 

1249 # actually apply the mask 

1250 self.vertices = self.vertices[mask] 

1251 

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

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

1254 try: 

1255 self.vertex_normals = cached_normals[mask] 

1256 except BaseException: 

1257 pass 

1258 

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

1260 """ 

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

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

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

1264 as well as keeping track of normals and colors. 

1265 

1266 Parameters 

1267 ------------ 

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

1269 Mask to remove faces 

1270 """ 

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

1272 if self.is_empty: 

1273 return 

1274 

1275 mask = np.asanyarray(mask) 

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

1277 # mask removes no faces so exit early 

1278 return 

1279 

1280 # try to save face normals before dumping cache 

1281 cached_normals = self._cache["face_normals"] 

1282 

1283 faces = self._data["faces"] 

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

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

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

1287 faces = self._cache["faces"] 

1288 

1289 # apply to face_attributes 

1290 count = len(self.faces) 

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

1292 try: 

1293 # covers un-len'd objects as well 

1294 if len(value) != count: 

1295 raise TypeError() 

1296 except TypeError: 

1297 continue 

1298 # apply the mask to the attribute 

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

1300 

1301 # actually apply the mask 

1302 self.faces = faces[mask] 

1303 

1304 # apply to face colors 

1305 self.visual.update_faces(mask) 

1306 

1307 # if our normals were the correct shape apply them 

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

1309 self.face_normals = cached_normals[mask] 

1310 

1311 def extend_faces(self, new_faces: ArrayLike): 

1312 """ 

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

1314 

1315 This does substantial bookkeeping: padding face colors 

1316 and face attributes with default values, and preserving cached 

1317 face normals to avoid recomputing every normal. 

1318 

1319 Parameters 

1320 ------------ 

1321 new_faces : (n, 3) integer 

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

1323 """ 

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

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

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

1327 

1328 if len(new_faces) == 0: 

1329 return 

1330 

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

1332 self._cache.verify() 

1333 

1334 # if we manage to extend colors and normals 

1335 extend_normals, extend_colors = None, None 

1336 

1337 # always filter degenerate triangles 

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

1339 new_faces = new_faces[valid] 

1340 if len(new_faces) == 0: 

1341 return 

1342 

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

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

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

1346 if len(cached_normals) > 0: 

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

1348 

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

1350 extend_colors = util.vstack_empty( 

1351 ( 

1352 self.visual.face_colors, 

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

1354 ) 

1355 ) 

1356 

1357 ########## 

1358 # DO ALL MUTATION AT THE END HERE 

1359 # apply the new faces 

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

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

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

1363 self._cache.verify() 

1364 # save us a normals recompute if we can 

1365 if extend_normals is not None: 

1366 self._cache["face_normals"] = extend_normals 

1367 if extend_colors is not None: 

1368 self.visual.face_colors = extend_colors 

1369 

1370 # collect new, padded face attributes 

1371 new_attribs = {} 

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

1373 shape = np.shape(attrib) 

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

1375 continue 

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

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

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

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

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

1381 # update outside the loop with new values 

1382 self.face_attributes.update(new_attribs) 

1383 

1384 def remove_infinite_values(self) -> None: 

1385 """ 

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

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

1388 

1389 Alters `self.faces` and `self.vertices` 

1390 """ 

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

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

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

1394 if util.is_shape(self.faces, (-1, 3)) and not vertex_mask.all(): 

1395 # drop faces touching a removed vertex before reindexing 

1396 # maps them to a degenerate triangle, #2445 — empty faces 

1397 # and all-true masks early-return inside `update_faces` 

1398 self.update_faces(vertex_mask[self.faces].all(axis=1)) 

1399 self.update_vertices(vertex_mask) 

1400 

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

1402 """ 

1403 On the current mesh find which faces are unique. 

1404 

1405 Returns 

1406 -------- 

1407 unique : (len(faces),) bool 

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

1409 """ 

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

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

1412 return mask 

1413 

1414 def rezero(self) -> None: 

1415 """ 

1416 Translate the mesh so that all vertex vertices are positive 

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

1418 

1419 Alters `self.vertices`. 

1420 """ 

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

1422 

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

1424 """ 

1425 Split a mesh into multiple meshes from face 

1426 connectivity. 

1427 

1428 If only_watertight is true it will only return 

1429 watertight meshes and will attempt to repair 

1430 single triangle or quad holes. 

1431 

1432 Parameters 

1433 ---------- 

1434 mesh : trimesh.Trimesh 

1435 The source multibody mesh to split 

1436 only_watertight 

1437 Only return watertight components and discard 

1438 any connected component that isn't fully watertight. 

1439 repair 

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

1441 discard step in `only_watertight. 

1442 adjacency : (n, 2) int 

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

1444 engine 

1445 Which graph engine to use for the connected components. 

1446 kwargs 

1447 Will be passed to `mesh.submesh` 

1448 

1449 Returns 

1450 ---------- 

1451 meshes : (m,) trimesh.Trimesh 

1452 Results of splitting based on parameters. 

1453 """ 

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

1455 

1456 @cache_decorator 

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

1458 """ 

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

1460 

1461 Returns 

1462 ---------- 

1463 adjacency : (n, 2) int 

1464 Pairs of faces which share an edge 

1465 

1466 Examples 

1467 --------- 

1468 

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

1470 

1471 In [2]: mesh.face_adjacency 

1472 Out[2]: 

1473 array([[ 0, 1], 

1474 [ 2, 3], 

1475 [ 0, 3], 

1476 ..., 

1477 [1112, 949], 

1478 [3467, 3475], 

1479 [1113, 3475]]) 

1480 

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

1482 Out[3]: 

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

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

1485 

1486 In [4]: import networkx as nx 

1487 

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

1489 

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

1491 """ 

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

1493 self._cache["face_adjacency_edges"] = edges 

1494 return adjacency 

1495 

1496 @cache_decorator 

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

1498 """ 

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

1500 

1501 Returns 

1502 ---------- 

1503 neighborhood : (n, 2) int 

1504 Pairs of faces which share a vertex 

1505 """ 

1506 return graph.face_neighborhood(self) 

1507 

1508 @cache_decorator 

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

1510 """ 

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

1512 

1513 Returns 

1514 -------- 

1515 edges : (n, 2) int 

1516 Vertex indices which correspond to face_adjacency 

1517 """ 

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

1519 _ = self.face_adjacency 

1520 return self._cache["face_adjacency_edges"] 

1521 

1522 @cache_decorator 

1523 def face_adjacency_edges_tree(self) -> cKDTree: 

1524 """ 

1525 A KDTree for mapping edges back face adjacency index. 

1526 

1527 Returns 

1528 ------------ 

1529 tree : scipy.spatial.cKDTree 

1530 Tree when queried with SORTED edges will return 

1531 their index in mesh.face_adjacency 

1532 """ 

1533 return cKDTree(self.face_adjacency_edges) 

1534 

1535 @cache_decorator 

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

1537 """ 

1538 Return the unsigned angle between adjacent faces 

1539 in radians. 

1540 

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

1542 use the `face_adjacency_convex` attribute to get a 

1543 signed angle with advanced indexing: 

1544 

1545 ``` 

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

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

1548 

1549 # apply the signs to the angles 

1550 angles = mesh.face_adjacency_angles * signs 

1551 ``` 

1552 

1553 Returns 

1554 -------- 

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

1556 Unsigned angle between adjacent faces 

1557 corresponding with `self.face_adjacency` 

1558 """ 

1559 # get pairs of unit vectors for adjacent faces 

1560 pairs = self.face_normals[self.face_adjacency] 

1561 # find the angle between the pairs of vectors 

1562 angles = geometry.vector_angle(pairs) 

1563 return angles 

1564 

1565 @cache_decorator 

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

1567 """ 

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

1569 its adjacent face 

1570 

1571 Returns 

1572 ---------- 

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

1574 Dot product of vertex 

1575 onto plane of adjacent triangle. 

1576 """ 

1577 projections = convex.adjacency_projections(self) 

1578 return projections 

1579 

1580 @cache_decorator 

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

1582 """ 

1583 Return faces which are adjacent and locally convex. 

1584 

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

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

1587 has a projection that is zero or negative. 

1588 

1589 Returns 

1590 ---------- 

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

1592 Face pairs that are locally convex 

1593 """ 

1594 return self.face_adjacency_projections < tol.merge 

1595 

1596 @cache_decorator 

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

1598 """ 

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

1600 edge between two adjacent faces 

1601 

1602 Returns 

1603 ----------- 

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

1605 Indexes of mesh.vertices 

1606 """ 

1607 return graph.face_adjacency_unshared(self) 

1608 

1609 @cache_decorator 

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

1611 """ 

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

1613 

1614 Returns 

1615 ------------ 

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

1617 Approximate radius formed by triangle pair 

1618 """ 

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

1620 return radii 

1621 

1622 @cache_decorator 

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

1624 """ 

1625 The approximate perpendicular projection of the non-shared 

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

1627 the two faces. 

1628 

1629 Returns 

1630 ------------ 

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

1632 Approximate span between the non-shared vertices 

1633 """ 

1634 _ = self.face_adjacency_radius 

1635 return self._cache["face_adjacency_span"] 

1636 

1637 @cache_decorator 

1638 def integral_mean_curvature(self) -> float64: 

1639 """ 

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

1641 

1642 Returns 

1643 --------- 

1644 area : float 

1645 Integral mean curvature of mesh 

1646 """ 

1647 edges_length = np.linalg.norm( 

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

1649 ) 

1650 # assign signs based on convex adjacency of face pairs 

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

1652 # adjust face adjacency angles with signs to reflect orientation 

1653 angles = self.face_adjacency_angles * signs 

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

1655 

1656 @cache_decorator 

1657 def vertex_adjacency_graph(self) -> Graph: 

1658 """ 

1659 Returns a networkx graph representing the vertices and their connections 

1660 in the mesh. 

1661 

1662 Returns 

1663 --------- 

1664 graph: networkx.Graph 

1665 Graph representing vertices and edges between 

1666 them where vertices are nodes and edges are edges 

1667 

1668 Examples 

1669 ---------- 

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

1671 potentially for some simple smoothing techniques. 

1672 

1673 mesh = trimesh.primitives.Box() 

1674 graph = mesh.vertex_adjacency_graph 

1675 graph.neighbors(0) 

1676 > [1, 2, 3, 4] 

1677 """ 

1678 

1679 return graph.vertex_adjacency_graph(mesh=self) 

1680 

1681 @cache_decorator 

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

1683 """ 

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

1685 the cached vertex_adjacency_graph, if already existent. 

1686 

1687 Returns 

1688 ---------- 

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

1690 Represents immediate neighbors of each vertex along 

1691 the edge of a triangle 

1692 

1693 Examples 

1694 ---------- 

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

1696 potentially for some simple smoothing techniques. 

1697 

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

1699 >>> mesh.vertex_neighbors[0] 

1700 [1, 2, 3, 4] 

1701 """ 

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

1703 

1704 @cache_decorator 

1705 def is_winding_consistent(self) -> bool: 

1706 """ 

1707 Does the mesh have consistent winding or not. 

1708 A mesh with consistent winding has each shared edge 

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

1710 

1711 Returns 

1712 -------- 

1713 consistent : bool 

1714 Is winding is consistent or not 

1715 """ 

1716 if self.is_empty: 

1717 return False 

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

1719 _ = self.is_watertight 

1720 return self._cache["is_winding_consistent"] 

1721 

1722 @cache_decorator 

1723 def is_watertight(self) -> bool: 

1724 """ 

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

1726 included in two faces. 

1727 

1728 Returns 

1729 ---------- 

1730 is_watertight : bool 

1731 Is mesh watertight or not 

1732 """ 

1733 if self.is_empty: 

1734 return False 

1735 watertight, winding = graph.is_watertight( 

1736 edges=self.edges, edges_sorted=self.edges_sorted 

1737 ) 

1738 self._cache["is_winding_consistent"] = winding 

1739 return watertight 

1740 

1741 @cache_decorator 

1742 def is_volume(self) -> bool: 

1743 """ 

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

1745 a valid volume, rather than just a surface. 

1746 

1747 These properties include being watertight, having consistent 

1748 winding and outward facing normals. 

1749 

1750 Returns 

1751 --------- 

1752 valid 

1753 Does the mesh represent a volume 

1754 """ 

1755 return bool( 

1756 self.is_watertight 

1757 and self.is_winding_consistent 

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

1759 and self.volume > 0.0 

1760 ) 

1761 

1762 @property 

1763 def is_empty(self) -> bool: 

1764 """ 

1765 Does the current mesh have data defined. 

1766 

1767 Returns 

1768 -------- 

1769 empty : bool 

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

1771 """ 

1772 return self._data.is_empty() 

1773 

1774 @cache_decorator 

1775 def is_convex(self) -> bool: 

1776 """ 

1777 Check if a mesh is convex or not. 

1778 

1779 Returns 

1780 ---------- 

1781 is_convex: bool 

1782 Is mesh convex or not 

1783 """ 

1784 if self.is_empty: 

1785 return False 

1786 

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

1788 return is_convex 

1789 

1790 @cache_decorator 

1791 def kdtree(self) -> cKDTree: 

1792 """ 

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

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

1795 

1796 Returns 

1797 --------- 

1798 tree : scipy.spatial.cKDTree 

1799 Contains mesh.vertices 

1800 """ 

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

1802 

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

1804 """ 

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

1806 in the current mesh. 

1807 

1808 Usage example for removing them: 

1809 `mesh.update_faces(mesh.nondegenerate_faces())` 

1810 

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

1812 bounding box with one edge shorter than that height. 

1813 

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

1815 

1816 Parameters 

1817 ------------ 

1818 height : float 

1819 If specified identifies faces with an oriented bounding 

1820 box shorter than this on one side. 

1821 

1822 Returns 

1823 ------------- 

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

1825 Mask that can be used to remove faces 

1826 """ 

1827 return triangles.nondegenerate( 

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

1829 ) 

1830 

1831 @cache_decorator 

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

1833 """ 

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

1835 

1836 Returns 

1837 --------- 

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

1839 Groups of indexes of self.faces 

1840 """ 

1841 facets = graph.facets(self) 

1842 return facets 

1843 

1844 @cache_decorator 

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

1846 """ 

1847 Return an array containing the area of each facet. 

1848 

1849 Returns 

1850 --------- 

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

1852 Total area of each facet (group of faces) 

1853 """ 

1854 # avoid thrashing the cache inside a loop 

1855 area_faces = self.area_faces 

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

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

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

1859 # native sum provides roughly a 50% speedup 

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

1861 return areas 

1862 

1863 @cache_decorator 

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

1865 """ 

1866 Return the normal of each facet 

1867 

1868 Returns 

1869 --------- 

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

1871 A unit normal vector for each facet 

1872 """ 

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

1874 return np.array([]) 

1875 

1876 area_faces = self.area_faces 

1877 

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

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

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

1881 normals = self.face_normals[index] 

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

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

1884 # save origins in cache 

1885 self._cache["facets_origin"] = origins 

1886 

1887 return normals 

1888 

1889 @cache_decorator 

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

1891 """ 

1892 Return a point on the facet plane. 

1893 

1894 Returns 

1895 ------------ 

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

1897 A point on each facet plane 

1898 """ 

1899 _ = self.facets_normal 

1900 return self._cache["facets_origin"] 

1901 

1902 @cache_decorator 

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

1904 """ 

1905 Return the edges which represent the boundary of each facet 

1906 

1907 Returns 

1908 --------- 

1909 edges_boundary : sequence of (n, 2) int 

1910 Indices of self.vertices 

1911 """ 

1912 # make each row correspond to a single face 

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

1914 # get the edges for each facet 

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

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

1917 return edges_boundary 

1918 

1919 @cache_decorator 

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

1921 """ 

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

1923 

1924 Returns 

1925 --------- 

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

1927 is A facet on the meshes convex hull or not 

1928 """ 

1929 # if no facets exit early 

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

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

1932 

1933 # facets plane, origin and normal 

1934 normals = self.facets_normal 

1935 origins = self.facets_origin 

1936 

1937 # (n, 3) convex hull vertices 

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

1939 

1940 # boolean mask for which facets are on convex hull 

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

1942 

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

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

1945 # of the convex hull is behind that plane 

1946 # which we are checking with dot products 

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

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

1949 

1950 return on_hull 

1951 

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

1953 """ 

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

1955 winding direction. 

1956 

1957 For face normals ensure that vectors are consistently pointed 

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

1959 for all connected components. 

1960 

1961 Parameters 

1962 ------------- 

1963 multibody : None or bool 

1964 Fix normals across multiple bodies or if unspecified 

1965 check the current `Trimesh.body_count`. 

1966 """ 

1967 if multibody is None: 

1968 multibody = self.body_count > 1 

1969 repair.fix_normals(self, multibody=multibody) 

1970 return self 

1971 

1972 def fill_holes(self) -> bool: 

1973 """ 

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

1975 

1976 Returns 

1977 ---------- 

1978 watertight : bool 

1979 Is the mesh watertight after the function completes 

1980 """ 

1981 return repair.fill_holes(self) 

1982 

1983 def register( 

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

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

1986 """ 

1987 Align a mesh with another mesh or a PointCloud using 

1988 the principal axes of inertia as a starting point which 

1989 is refined by iterative closest point. 

1990 

1991 Parameters 

1992 ------------ 

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

1994 Mesh or points in space 

1995 samples : int 

1996 Number of samples from mesh surface to align 

1997 icp_first : int 

1998 How many ICP iterations for the 9 possible 

1999 combinations of 

2000 icp_final : int 

2001 How many ICP itertations for the closest 

2002 candidate from the wider search 

2003 

2004 Returns 

2005 ----------- 

2006 mesh_to_other : (4, 4) float 

2007 Transform to align mesh to the other object 

2008 cost : float 

2009 Average square distance per point 

2010 """ 

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

2012 return mesh_to_other, cost 

2013 

2014 def compute_stable_poses( 

2015 self, 

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

2017 sigma: Floating = 0.0, 

2018 n_samples: Integer = 1, 

2019 threshold: Floating = 0.0, 

2020 seed: Seed = None, 

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

2022 """ 

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

2024 

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

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

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

2028 a planar workspace and evaluates the probabilities of landing in 

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

2030 

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

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

2033 and a list of the probabilities for each pose. 

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

2035 most probable pose first. 

2036 

2037 Parameters 

2038 ------------ 

2039 center_mass : (3, ) float 

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

2041 assumes uniform density and watertightness and 

2042 computes a center of mass explicitly) 

2043 sigma : float 

2044 The covariance for the multivariate gaussian used 

2045 to sample center of mass locations 

2046 n_samples : int 

2047 The number of samples of the center of mass location 

2048 threshold : float 

2049 The probability value at which to threshold 

2050 returned stable poses 

2051 

2052 Returns 

2053 ------- 

2054 transforms : (n, 4, 4) float 

2055 The homogeneous matrices that transform the 

2056 object to rest in a stable pose, with the 

2057 new z-axis pointing upwards from the table 

2058 and the object just touching the table. 

2059 

2060 probs : (n, ) float 

2061 A probability ranging from 0.0 to 1.0 for each pose 

2062 """ 

2063 return poses.compute_stable_poses( 

2064 mesh=self, 

2065 center_mass=center_mass, 

2066 sigma=sigma, 

2067 n_samples=n_samples, 

2068 threshold=threshold, 

2069 seed=seed, 

2070 ) 

2071 

2072 def subdivide( 

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

2074 ) -> "Trimesh": 

2075 """ 

2076 Subdivide a mesh with each subdivided face replaced 

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

2078 mesh with subdivided faces. 

2079 

2080 Parameters 

2081 ------------ 

2082 face_index : (m, ) int or None 

2083 If None all faces of mesh will be subdivided 

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

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

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

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

2088 and an additional postprocessing step will be required to 

2089 make resulting mesh watertight 

2090 iterations : int 

2091 If passed will run subdivisions multiple times recursively. 

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

2093 if both arguments are passed. 

2094 

2095 Returns 

2096 ------------ 

2097 mesh: trimesh.Trimesh 

2098 The copy of current mesh with subdivided faces. 

2099 """ 

2100 if iterations is not None and face_index is not None: 

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

2102 

2103 visual = None 

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

2105 len(self.vertices), 

2106 2, 

2107 ): 

2108 # uv coords divided along with vertices 

2109 vertices, faces, attr = remesh.subdivide( 

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

2111 faces=self.faces, 

2112 face_index=face_index, 

2113 vertex_attributes=self.vertex_attributes, 

2114 ) 

2115 

2116 # get a copy of the current visuals 

2117 visual = self.visual.copy() 

2118 

2119 # separate uv coords and vertices 

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

2121 

2122 else: 

2123 # perform the subdivision with vertex attributes 

2124 vertices, faces, attr = remesh.subdivide( 

2125 vertices=self.vertices, 

2126 faces=self.faces, 

2127 face_index=face_index, 

2128 vertex_attributes=self.vertex_attributes, 

2129 ) 

2130 

2131 # create a new mesh 

2132 result = Trimesh( 

2133 vertices=vertices, 

2134 faces=faces, 

2135 visual=visual, 

2136 vertex_attributes=attr, 

2137 process=False, 

2138 ) 

2139 

2140 if iterations is not None and iterations > 1: 

2141 return result.subdivide(iterations=iterations - 1) 

2142 

2143 return result 

2144 

2145 def subdivide_to_size( 

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

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

2148 """ 

2149 Subdivide a mesh until every edge is shorter than a 

2150 specified length. 

2151 

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

2153 

2154 Parameters 

2155 ------------ 

2156 max_edge 

2157 Maximum length of any edge in the result 

2158 max_iter : int 

2159 The maximum number of times to run subdivision 

2160 return_index : bool 

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

2162 

2163 Returns 

2164 ------------ 

2165 mesh: trimesh.Trimesh 

2166 The copy of current mesh with subdivided faces. 

2167 """ 

2168 # subdivide vertex attributes 

2169 visual = None 

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

2171 len(self.vertices), 

2172 2, 

2173 ): 

2174 # uv coords divided along with vertices 

2175 vertices_faces = remesh.subdivide_to_size( 

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

2177 faces=self.faces, 

2178 max_edge=max_edge, 

2179 max_iter=max_iter, 

2180 return_index=return_index, 

2181 ) 

2182 # unpack result 

2183 if return_index: 

2184 vertices, faces, final_index = vertices_faces 

2185 else: 

2186 vertices, faces = vertices_faces 

2187 

2188 # get a copy of the current visuals 

2189 visual = self.visual.copy() 

2190 

2191 # separate uv coords and vertices 

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

2193 

2194 else: 

2195 # uv coords divided along with vertices 

2196 vertices_faces = remesh.subdivide_to_size( 

2197 vertices=self.vertices, 

2198 faces=self.faces, 

2199 max_edge=max_edge, 

2200 max_iter=max_iter, 

2201 return_index=return_index, 

2202 ) 

2203 # unpack result 

2204 if return_index: 

2205 vertices, faces, final_index = vertices_faces 

2206 else: 

2207 vertices, faces = vertices_faces 

2208 

2209 # create a new mesh 

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

2211 

2212 if return_index: 

2213 return result, final_index 

2214 

2215 return result 

2216 

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

2218 """ 

2219 Subdivide a mesh by dividing each triangle into four 

2220 triangles and approximating their smoothed surface 

2221 using loop subdivision. Loop subdivision often looks 

2222 better on triangular meshes than catmul-clark, which 

2223 operates primarily on quads. 

2224 

2225 Parameters 

2226 ------------ 

2227 iterations : int 

2228 Number of iterations to run subdivision. 

2229 multibody : bool 

2230 If True will try to subdivide for each submesh 

2231 

2232 Returns 

2233 ------------ 

2234 mesh: trimesh.Trimesh 

2235 The copy of current mesh with subdivided faces. 

2236 """ 

2237 # perform subdivision for one mesh 

2238 new_vertices, new_faces = remesh.subdivide_loop( 

2239 vertices=self.vertices, faces=self.faces, iterations=iterations 

2240 ) 

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

2242 

2243 @property 

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

2245 """ 

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

2247 this function will disconnect regions above an angle threshold 

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

2249 in an OpenGL rendering context. 

2250 

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

2252 

2253 Returns 

2254 --------- 

2255 smooth_shaded : trimesh.Trimesh 

2256 Non watertight version of current mesh. 

2257 """ 

2258 # key this also by the visual properties 

2259 # but store it in the mesh cache 

2260 self.visual._verify_hash() 

2261 cache = self.visual._cache 

2262 # needs to be dumped whenever visual or mesh changes 

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

2264 if key in cache: 

2265 return cache[key] 

2266 smooth = graph.smooth_shade(self) 

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

2268 cache[key] = smooth 

2269 return smooth 

2270 

2271 @property 

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

2273 """ 

2274 Get the stored visuals for the current mesh. 

2275 

2276 Returns 

2277 ------------- 

2278 visual : ColorVisuals or TextureVisuals 

2279 Contains visual information about the mesh 

2280 """ 

2281 if hasattr(self, "_visual"): 

2282 return self._visual 

2283 return None 

2284 

2285 @visual.setter 

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

2287 """ 

2288 When setting a visual object, always make sure 

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

2290 

2291 Parameters 

2292 -------------- 

2293 visual : ColorVisuals or TextureVisuals 

2294 Contains visual information about the mesh 

2295 """ 

2296 if value is None: 

2297 value = ColorVisuals() 

2298 value.mesh = self 

2299 self._visual = value 

2300 

2301 def section( 

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

2303 ) -> Path3D | None: 

2304 """ 

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

2306 defined by origin and normal. 

2307 

2308 Parameters 

2309 ------------ 

2310 plane_normal : (3,) float 

2311 Normal vector of section plane. 

2312 plane_origin : (3, ) float 

2313 Point on the cross section plane. 

2314 

2315 Returns 

2316 --------- 

2317 intersections 

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

2319 """ 

2320 # turn line segments into Path2D/Path3D objects 

2321 from .path.exchange.misc import lines_to_path 

2322 from .path.path import Path3D 

2323 

2324 # return a single cross section in 3D 

2325 lines, _face_index = intersections.mesh_plane( 

2326 mesh=self, 

2327 plane_normal=plane_normal, 

2328 plane_origin=plane_origin, 

2329 return_faces=True, 

2330 **kwargs, 

2331 ) 

2332 

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

2334 if len(lines) == 0: 

2335 return None 

2336 

2337 # otherwise load the line segments into the keyword arguments 

2338 # for a Path3D object. 

2339 path = lines_to_path(lines) 

2340 

2341 # add the face index info into metadata 

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

2343 

2344 return Path3D(**path) 

2345 

2346 def section_multiplane( 

2347 self, 

2348 plane_origin: ArrayLike, 

2349 plane_normal: ArrayLike, 

2350 heights: ArrayLike, 

2351 ) -> list[Path2D | None]: 

2352 """ 

2353 Return multiple parallel cross sections of the current 

2354 mesh in 2D. 

2355 

2356 Parameters 

2357 ------------ 

2358 plane_origin : (3, ) float 

2359 Point on the cross section plane 

2360 plane_normal : (3) float 

2361 Normal vector of section plane 

2362 heights : (n, ) float 

2363 Each section is offset by height along 

2364 the plane normal. 

2365 

2366 Returns 

2367 --------- 

2368 paths : (n, ) Path2D or None 

2369 2D cross sections at specified heights. 

2370 path.metadata['to_3D'] contains transform 

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

2372 """ 

2373 # turn line segments into Path2D/Path3D objects 

2374 from .exchange.load import load_path 

2375 

2376 # do a multiplane intersection 

2377 lines, transforms, faces = intersections.mesh_multiplane( 

2378 mesh=self, 

2379 plane_normal=plane_normal, 

2380 plane_origin=plane_origin, 

2381 heights=heights, 

2382 ) 

2383 

2384 # turn the line segments into Path2D objects 

2385 paths = [None] * len(lines) 

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

2387 if len(segments) > 0: 

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

2389 return paths 

2390 

2391 def slice_plane( 

2392 self, 

2393 plane_origin: ArrayLike, 

2394 plane_normal: ArrayLike, 

2395 cap: bool = False, 

2396 face_index: ArrayLike | None = None, 

2397 **kwargs, 

2398 ) -> "Trimesh": 

2399 """ 

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

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

2402 

2403 plane_origin : (3,) float 

2404 Point on plane to intersect with mesh 

2405 plane_normal : (3,) float 

2406 Normal vector of plane to intersect with mesh 

2407 cap : bool 

2408 If True, cap the result with a triangulated polygon 

2409 face_index : ((m,) int) 

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

2411 provided, the default is to slice all faces. 

2412 

2413 Returns 

2414 --------- 

2415 new_mesh: trimesh.Trimesh or None 

2416 Subset of current mesh that intersects the half plane 

2417 to the positive normal side of the plane 

2418 """ 

2419 

2420 # return a new mesh 

2421 new_mesh = intersections.slice_mesh_plane( 

2422 mesh=self, 

2423 plane_normal=plane_normal, 

2424 plane_origin=plane_origin, 

2425 cap=cap, 

2426 face_index=face_index, 

2427 **kwargs, 

2428 ) 

2429 

2430 return new_mesh 

2431 

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

2433 """ 

2434 Returns a Trimesh object equivalent to the current mesh where 

2435 the vertices have been assigned uv texture coordinates. Vertices 

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

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

2438 

2439 Requires `pip install xatlas` 

2440 

2441 Parameters 

2442 ------------ 

2443 image : None or PIL.Image 

2444 Image to assign to the material 

2445 

2446 Returns 

2447 -------- 

2448 unwrapped : trimesh.Trimesh 

2449 Mesh with unwrapped uv coordinates 

2450 """ 

2451 import xatlas 

2452 

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

2454 

2455 result = Trimesh( 

2456 vertices=self.vertices[vmap], 

2457 faces=faces, 

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

2459 process=False, 

2460 ) 

2461 

2462 # run additional checks for unwrapping 

2463 if tol.strict: 

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

2465 # move the indices around on creation 

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

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

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

2469 # check to make sure indices are still the 

2470 # same order after we've exported to OBJ 

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

2472 uv_recon = np.array( 

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

2474 dtype=float64, 

2475 ) 

2476 assert np.allclose(uv_recon, uv) 

2477 v_recon = np.array( 

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

2479 dtype=float64, 

2480 ) 

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

2482 

2483 return result 

2484 

2485 @cache_decorator 

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

2487 """ 

2488 Returns a Trimesh object representing the convex hull of 

2489 the current mesh. 

2490 

2491 Returns 

2492 -------- 

2493 convex : trimesh.Trimesh 

2494 Mesh of convex hull of current mesh 

2495 """ 

2496 return convex.convex_hull(self) 

2497 

2498 def sample( 

2499 self, 

2500 count: Integer, 

2501 return_index: bool = False, 

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

2503 seed: Seed = None, 

2504 ): 

2505 """ 

2506 Return random samples distributed across the 

2507 surface of the mesh 

2508 

2509 Parameters 

2510 ------------ 

2511 count : int 

2512 Number of points to sample 

2513 return_index : bool 

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

2515 sample was taken from. 

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

2517 Weight faces by a factor other than face area. 

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

2519 seed : None or int 

2520 Seed for deterministic results, otherwise OS entropy. 

2521 

2522 Returns 

2523 --------- 

2524 samples : (count, 3) float 

2525 Points on surface of mesh 

2526 face_index : (count, ) int 

2527 Index of self.faces 

2528 """ 

2529 samples, index = sample.sample_surface( 

2530 mesh=self, count=count, face_weight=face_weight, seed=seed 

2531 ) 

2532 if return_index: 

2533 return samples, index 

2534 return samples 

2535 

2536 def remove_unreferenced_vertices(self) -> None: 

2537 """ 

2538 Remove all vertices in the current mesh which are not 

2539 referenced by a face. 

2540 """ 

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

2542 referenced[self.faces] = True 

2543 

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

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

2546 

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

2548 

2549 def unmerge_vertices(self) -> None: 

2550 """ 

2551 Removes all face references so that every face contains 

2552 three unique vertex indices and no faces are adjacent. 

2553 """ 

2554 # new faces are incrementing so every vertex is unique 

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

2556 

2557 # use update_vertices to apply mask to 

2558 # all properties that are per-vertex 

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

2560 # set faces to incrementing indexes 

2561 self.faces = faces 

2562 # keep face normals as the haven't changed 

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

2564 

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

2566 """ 

2567 Transform mesh by a homogeneous transformation matrix. 

2568 

2569 Does the bookkeeping to avoid recomputing things so this function 

2570 should be used rather than directly modifying self.vertices 

2571 if possible. 

2572 

2573 Parameters 

2574 ------------ 

2575 matrix : (4, 4) float 

2576 Homogeneous transformation matrix 

2577 """ 

2578 # get c-order float64 matrix 

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

2580 

2581 # only support homogeneous transformations 

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

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

2584 

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

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

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

2588 return self 

2589 

2590 # new vertex positions 

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

2592 

2593 # check to see if the matrix has rotation 

2594 # rather than just translation 

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

2596 

2597 # transform overridden center of mass 

2598 if "center_mass" in self._data: 

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

2600 self.center_mass = transformations.transform_points( 

2601 center_mass, 

2602 matrix, 

2603 )[0] 

2604 

2605 # preserve face normals if we have them stored 

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

2607 # transform face normals by rotation component 

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

2609 transformations.transform_points( 

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

2611 ) 

2612 ) 

2613 

2614 # preserve vertex normals if we have them stored 

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

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

2617 transformations.transform_points( 

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

2619 ) 

2620 ) 

2621 

2622 # if transformation flips winding of triangles 

2623 if has_rotation and transformations.flips_winding(matrix): 

2624 log.debug("transform flips winding") 

2625 # fliplr will make array non C contiguous 

2626 # which will cause hashes to be more 

2627 # expensive than necessary so wrap 

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

2629 

2630 # assign the new values 

2631 self.vertices = new_vertices 

2632 

2633 # preserve normals and topology in cache 

2634 # while dumping everything else 

2635 self._cache.clear( 

2636 exclude={ 

2637 "face_normals", # transformed by us 

2638 "vertex_normals", # also transformed by us 

2639 "face_adjacency", # topological 

2640 "face_adjacency_edges", 

2641 "face_adjacency_unshared", 

2642 "edges", 

2643 "edges_face", 

2644 "edges_sorted", 

2645 "edges_unique", 

2646 "edges_unique_idx", 

2647 "edges_unique_inverse", 

2648 "edges_sparse", 

2649 "body_count", 

2650 "faces_unique_edges", 

2651 "euler_number", 

2652 } 

2653 ) 

2654 # set the cache ID with the current hash value 

2655 self._cache.id_set() 

2656 return self 

2657 

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

2659 """ 

2660 Return a VoxelGrid object representing the current mesh 

2661 discretized into voxels at the specified pitch 

2662 

2663 Parameters 

2664 ------------ 

2665 pitch : float 

2666 The edge length of a single voxel 

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

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

2669 

2670 Returns 

2671 ---------- 

2672 voxelized : VoxelGrid object 

2673 Representing the current mesh 

2674 """ 

2675 from .voxel import creation 

2676 

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

2678 

2679 def simplify_quadric_decimation( 

2680 self, 

2681 percent: Floating | None = None, 

2682 face_count: Integer | None = None, 

2683 aggression: Integer | None = None, 

2684 ) -> "Trimesh": 

2685 """ 

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

2687 

2688 Parameters 

2689 ----------- 

2690 percent 

2691 A number between 0.0 and 1.0 for how much 

2692 face_count 

2693 Target number of faces desired in the resulting mesh. 

2694 aggression 

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

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

2697 

2698 Returns 

2699 --------- 

2700 simple : trimesh.Trimesh 

2701 Simplified version of mesh. 

2702 """ 

2703 from fast_simplification import simplify 

2704 

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

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

2707 # and requires they be omitted from the constructor 

2708 kwargs = { 

2709 "target_count": face_count, 

2710 "target_reduction": percent, 

2711 "agg": aggression, 

2712 } 

2713 

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

2715 # apply the same simplification to the visual info 

2716 vertices, faces = simplify( 

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

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

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

2720 ) 

2721 

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

2723 

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

2725 """ 

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

2727 faces and return it as a Path3D. 

2728 

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

2730 included by a single triangle. 

2731 

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

2733 outline of a watertight mesh is an empty path. 

2734 

2735 Parameters 

2736 ------------ 

2737 face_ids : (n, ) int 

2738 Indices to compute the outline of. 

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

2740 **kwargs: passed to Path3D constructor 

2741 

2742 Returns 

2743 ---------- 

2744 path : Path3D 

2745 Curve in 3D of the outline 

2746 """ 

2747 from .path.exchange.misc import faces_to_path 

2748 

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

2750 

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

2752 """ 

2753 Project a mesh onto a plane and then extract the 

2754 polygon that outlines the mesh projection on that 

2755 plane. 

2756 

2757 Parameters 

2758 ---------- 

2759 normal : (3,) float 

2760 Normal to extract flat pattern along 

2761 origin : None or (3,) float 

2762 Origin of plane to project mesh onto 

2763 ignore_sign : bool 

2764 Allow a projection from the normal vector in 

2765 either direction: this provides a substantial speedup 

2766 on watertight meshes where the direction is irrelevant 

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

2768 backfaces you should set this to False. 

2769 rpad : float 

2770 Proportion to pad polygons by before unioning 

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

2772 apad : float 

2773 Absolute padding to pad polygons by before unioning 

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

2775 tol_dot : float 

2776 Tolerance for discarding on-edge triangles. 

2777 precise : bool 

2778 Use the precise projection computation using shapely. 

2779 precise_eps : float 

2780 Tolerance for precise triangle checks. 

2781 

2782 Returns 

2783 ---------- 

2784 projected : trimesh.path.Path2D 

2785 Outline of source mesh 

2786 """ 

2787 from .exchange.load import load_path 

2788 from .path import Path2D 

2789 from .path.polygons import projected 

2790 

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

2792 if projection is None: 

2793 return Path2D() 

2794 return load_path(projection) 

2795 

2796 @cache_decorator 

2797 def area(self) -> float64: 

2798 """ 

2799 Summed area of all triangles in the current mesh. 

2800 

2801 Returns 

2802 --------- 

2803 area : float 

2804 Surface area of mesh 

2805 """ 

2806 area = self.area_faces.sum() 

2807 return area 

2808 

2809 @cache_decorator 

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

2811 """ 

2812 The area of each face in the mesh. 

2813 

2814 Returns 

2815 --------- 

2816 area_faces : (n, ) float 

2817 Area of each face 

2818 """ 

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

2820 

2821 @cache_decorator 

2822 def mass_properties(self) -> MassProperties: 

2823 """ 

2824 Returns the mass properties of the current mesh. 

2825 

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

2827 isn't watertight. 

2828 

2829 Returns 

2830 ---------- 

2831 properties : dict 

2832 With keys: 

2833 'volume' : in global units^3 

2834 'mass' : From specified density 

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

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

2837 coordinate system 

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

2839 """ 

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

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

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

2843 return triangles.mass_properties( 

2844 triangles=self.triangles, 

2845 crosses=self.triangles_cross, 

2846 density=density, 

2847 center_mass=center_mass, 

2848 skip_inertia=False, 

2849 ) 

2850 

2851 def invert(self) -> Self: 

2852 """ 

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

2854 face and negating normals without dumping the cache. 

2855 

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

2857 `self.face_normals` and `self.vertex_normals`. 

2858 """ 

2859 with self._cache: 

2860 if "face_normals" in self._cache: 

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

2862 if "vertex_normals" in self._cache: 

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

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

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

2866 # save our normals 

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

2868 

2869 return self 

2870 

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

2872 """ 

2873 Returns a Scene object containing the current mesh. 

2874 

2875 Returns 

2876 --------- 

2877 scene : trimesh.scene.scene.Scene 

2878 Contains just the current mesh 

2879 """ 

2880 return Scene(self, **kwargs) 

2881 

2882 def show( 

2883 self, 

2884 viewer: ViewerType = None, 

2885 **kwargs, 

2886 ) -> Scene: 

2887 """ 

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

2889 

2890 Parameters 

2891 ------------ 

2892 viewer : ViewerType 

2893 What kind of viewer to use, such as 

2894 `gl` to open a pyglet window 

2895 `jupyter` for a jupyter notebook 

2896 `marimo'` for a marimo notebook 

2897 None for a "best guess" 

2898 smooth : bool 

2899 Run smooth shading on mesh or not, 

2900 large meshes will be slow 

2901 

2902 Returns 

2903 ----------- 

2904 scene : trimesh.scene.Scene 

2905 Scene with current mesh in it 

2906 """ 

2907 scene = self.scene() 

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

2909 

2910 def submesh( 

2911 self, 

2912 faces_sequence: Sequence[ArrayLike], 

2913 only_watertight: bool = False, 

2914 repair: bool = False, 

2915 **kwargs, 

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

2917 """ 

2918 Return a subset of the mesh. 

2919 

2920 Parameters 

2921 ------------ 

2922 faces_sequence : sequence (m, ) int 

2923 Face indices of mesh 

2924 only_watertight : bool 

2925 Only return submeshes which are watertight 

2926 repair 

2927 Try to repair the submesh if it is not watertight 

2928 append : bool 

2929 Return a single mesh which has the faces appended. 

2930 if this flag is set, only_watertight is ignored 

2931 

2932 Returns 

2933 --------- 

2934 submesh : Trimesh or (n,) Trimesh 

2935 Single mesh if `append` or list of submeshes 

2936 """ 

2937 return util.submesh( 

2938 mesh=self, 

2939 faces_sequence=faces_sequence, 

2940 only_watertight=only_watertight, 

2941 repair=repair, 

2942 **kwargs, 

2943 ) 

2944 

2945 @cache_decorator 

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

2947 """ 

2948 Return a float vector which is unique to the mesh 

2949 and is robust to rotation and translation. 

2950 

2951 Returns 

2952 ----------- 

2953 identifier : (7,) float 

2954 Identifying properties of the current mesh 

2955 """ 

2956 return comparison.identifier_simple(self) 

2957 

2958 @cache_decorator 

2959 def identifier_hash(self) -> str: 

2960 """ 

2961 A hash of the rotation invariant identifier vector. 

2962 

2963 Returns 

2964 --------- 

2965 hashed : str 

2966 Hex string of the SHA256 hash from 

2967 the identifier vector at hand-tuned sigfigs. 

2968 """ 

2969 return comparison.identifier_hash(self.identifier) 

2970 

2971 def export( 

2972 self, 

2973 file_obj: Loadable = None, 

2974 file_type: str | None = None, 

2975 **kwargs, 

2976 ) -> dict | bytes | str: 

2977 """ 

2978 Export the current mesh to a file object. 

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

2980 

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

2982 dict, glb, dict64, msgpack. 

2983 

2984 Parameters 

2985 ------------ 

2986 file_obj : open writeable file object 

2987 str, file name where to save the mesh 

2988 None, return the export blob 

2989 file_type : str 

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

2991 is passed this is not required. 

2992 

2993 Returns 

2994 ---------- 

2995 exported : bytes or str 

2996 Result of exporter 

2997 """ 

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

2999 

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

3001 """ 

3002 Return a dictionary representation of the current mesh 

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

3004 Trimesh constructor and matches the schema in: 

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

3006 

3007 Returns 

3008 ---------- 

3009 result : dict 

3010 Matches schema and Trimesh constructor. 

3011 """ 

3012 return { 

3013 "kind": "trimesh", 

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

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

3016 } 

3017 

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

3019 """ 

3020 Compute an approximate convex decomposition of a mesh 

3021 using `pip install pyVHACD`. 

3022 

3023 Returns 

3024 ------- 

3025 meshes 

3026 List of convex meshes that approximate the original 

3027 **kwargs : VHACD keyword arguments 

3028 """ 

3029 return [ 

3030 Trimesh(**kwargs) 

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

3032 ] 

3033 

3034 def union( 

3035 self, 

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

3037 engine: BooleanEngineType = None, 

3038 check_volume: bool = True, 

3039 **kwargs, 

3040 ) -> "Trimesh": 

3041 """ 

3042 Boolean union between this mesh and other meshes. 

3043 

3044 Parameters 

3045 ------------ 

3046 other : Trimesh or (n, ) Trimesh 

3047 Other meshes to union 

3048 engine 

3049 Which backend to use, the default 

3050 recommendation is: `pip install manifold3d`. 

3051 check_volume 

3052 Raise an error if not all meshes are watertight 

3053 positive volumes. Advanced users may want to ignore 

3054 this check as it is expensive. 

3055 kwargs 

3056 Passed through to the `engine`. 

3057 

3058 Returns 

3059 --------- 

3060 union : trimesh.Trimesh 

3061 Union of self and other Trimesh objects 

3062 """ 

3063 return boolean.union( 

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

3065 engine=engine, 

3066 check_volume=check_volume, 

3067 **kwargs, 

3068 ) 

3069 

3070 def difference( 

3071 self, 

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

3073 engine: BooleanEngineType = None, 

3074 check_volume: bool = True, 

3075 **kwargs, 

3076 ) -> "Trimesh": 

3077 """ 

3078 Boolean difference between this mesh and other meshes. 

3079 

3080 Parameters 

3081 ------------ 

3082 other 

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

3084 engine 

3085 Which backend to use, the default 

3086 recommendation is: `pip install manifold3d`. 

3087 check_volume 

3088 Raise an error if not all meshes are watertight 

3089 positive volumes. Advanced users may want to ignore 

3090 this check as it is expensive. 

3091 kwargs 

3092 Passed through to the `engine`. 

3093 

3094 Returns 

3095 --------- 

3096 difference : trimesh.Trimesh 

3097 Difference between self and other Trimesh objects 

3098 """ 

3099 return boolean.difference( 

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

3101 engine=engine, 

3102 check_volume=check_volume, 

3103 **kwargs, 

3104 ) 

3105 

3106 def intersection( 

3107 self, 

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

3109 engine: BooleanEngineType = None, 

3110 check_volume: bool = True, 

3111 **kwargs, 

3112 ) -> "Trimesh": 

3113 """ 

3114 Boolean intersection between this mesh and other meshes. 

3115 

3116 Parameters 

3117 ------------ 

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

3119 Meshes to calculate intersections with 

3120 engine 

3121 Which backend to use, the default 

3122 recommendation is: `pip install manifold3d`. 

3123 check_volume 

3124 Raise an error if not all meshes are watertight 

3125 positive volumes. Advanced users may want to ignore 

3126 this check as it is expensive. 

3127 kwargs 

3128 Passed through to the `engine`. 

3129 

3130 Returns 

3131 --------- 

3132 intersection : trimesh.Trimesh 

3133 Mesh of the volume contained by all passed meshes 

3134 """ 

3135 return boolean.intersection( 

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

3137 engine=engine, 

3138 check_volume=check_volume, 

3139 **kwargs, 

3140 ) 

3141 

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

3143 """ 

3144 Given an array of points determine whether or not they 

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

3146 non-watertight mesh. 

3147 

3148 Parameters 

3149 ------------ 

3150 points : (n, 3) float 

3151 Points in cartesian space 

3152 

3153 Returns 

3154 --------- 

3155 contains : (n, ) bool 

3156 Whether or not each point is inside the mesh 

3157 """ 

3158 return self.ray.contains_points(points) 

3159 

3160 @cache_decorator 

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

3162 """ 

3163 Returns the angle at each vertex of a face. 

3164 

3165 Returns 

3166 -------- 

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

3168 Angle at each vertex of a face 

3169 """ 

3170 return triangles.angles(self.triangles) 

3171 

3172 @cache_decorator 

3173 def face_angles_sparse(self) -> coo_matrix: 

3174 """ 

3175 A sparse matrix representation of the face angles. 

3176 

3177 Returns 

3178 ---------- 

3179 sparse : scipy.sparse.coo_matrix 

3180 Float sparse matrix with with shape: 

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

3182 """ 

3183 angles = curvature.face_angles_sparse(self) 

3184 return angles 

3185 

3186 @cache_decorator 

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

3188 """ 

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

3190 of every face that includes that vertex. 

3191 

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

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

3194 concave negative. 

3195 

3196 Returns 

3197 -------- 

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

3199 Vertex defect at the every vertex 

3200 """ 

3201 defects = curvature.vertex_defects(self) 

3202 return defects 

3203 

3204 @cache_decorator 

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

3206 """ 

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

3208 

3209 Returns 

3210 ---------- 

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

3212 Number of faces each vertex is included in 

3213 """ 

3214 # get degree through sparse matrix 

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

3216 return degree 

3217 

3218 @cache_decorator 

3219 def face_adjacency_tree(self) -> Index: 

3220 """ 

3221 An R-tree of face adjacencies. 

3222 

3223 Returns 

3224 -------- 

3225 tree 

3226 Where each edge in self.face_adjacency has a 

3227 rectangular cell 

3228 """ 

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

3230 return util.bounds_tree( 

3231 np.column_stack( 

3232 ( 

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

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

3235 ) 

3236 ) 

3237 ) 

3238 

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

3240 """ 

3241 Safely return a copy of the current mesh. 

3242 

3243 By default, copied meshes will have emptied cache 

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

3245 operations until caches are regenerated. 

3246 

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

3248 

3249 Parameters 

3250 ------------ 

3251 include_cache : bool 

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

3253 include_visual : bool 

3254 If True, will copy visual information 

3255 

3256 Returns 

3257 --------- 

3258 copied : trimesh.Trimesh 

3259 Copy of current mesh 

3260 """ 

3261 # start with an empty mesh 

3262 copied = Trimesh() 

3263 # always deepcopy vertex and face data 

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

3265 

3266 if include_visual: 

3267 # copy visual information 

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

3269 

3270 copied.vertex_attributes.update( 

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

3272 ) 

3273 copied.face_attributes.update( 

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

3275 ) 

3276 

3277 # get metadata 

3278 copied.metadata = deepcopy(self.metadata) 

3279 

3280 # make sure cache ID is set initially 

3281 copied._cache.verify() 

3282 

3283 if include_cache: 

3284 # shallow copy cached items into the new cache 

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

3286 # data in the new mesh is changed these items 

3287 # will be dumped in the new mesh but preserved 

3288 # in the original mesh 

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

3290 

3291 return copied 

3292 

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

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

3295 return self.copy(include_cache=False) 

3296 

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

3298 # interpret shallow copy as "keep cached data" 

3299 return self.copy(include_cache=True) 

3300 

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

3302 """ 

3303 DEPRECATED: call `eval` directly instead. 

3304 

3305 Evaluate a statement and cache the result before returning. 

3306 

3307 Parameters 

3308 ------------ 

3309 statement : str 

3310 Statement of valid python code 

3311 *args : list 

3312 Available inside statement as args[0], etc 

3313 

3314 Returns 

3315 ----------- 

3316 result : result of running eval on statement with args 

3317 

3318 Examples 

3319 ----------- 

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

3321 """ 

3322 import warnings 

3323 

3324 warnings.warn( 

3325 "`Trimesh.eval_cached` is deprecated " 

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

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

3328 category=DeprecationWarning, 

3329 stacklevel=2, 

3330 ) 

3331 

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

3333 hashable = [hash(statement)] 

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

3335 

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

3337 

3338 if key in self._cache: 

3339 return self._cache[key] 

3340 

3341 result = eval(statement) 

3342 self._cache[key] = result 

3343 return result 

3344 

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

3346 """ 

3347 Concatenate the mesh with another mesh. 

3348 

3349 Parameters 

3350 ------------ 

3351 other : trimesh.Trimesh object 

3352 Mesh to be concatenated with self 

3353 

3354 Returns 

3355 ---------- 

3356 concat : trimesh.Trimesh 

3357 Mesh object of combined result 

3358 """ 

3359 concat = util.concatenate(self, other) 

3360 return concat