Coverage for trimesh/base.py: 93%

818 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-24 01:00 +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 mapping shared edges to face adjacency indices. 

1526 

1527 The tree contains only ``face_adjacency_edges``. Sort each pair of 

1528 vertex indices before querying. Edges outside this set, including 

1529 boundary edges, can return a nearest neighbor index. Check that the 

1530 query distance is close to zero before using the index to access 

1531 ``face_adjacency``. Distances are measured between vertex index pairs. 

1532 

1533 Returns 

1534 ------------ 

1535 tree : scipy.spatial.cKDTree 

1536 Tree of sorted vertex index pairs for shared edges. 

1537 

1538 Examples 

1539 -------- 

1540 Look up a shared edge and a boundary edge, keeping only the match: 

1541 

1542 >>> import numpy as np 

1543 >>> import trimesh 

1544 >>> mesh = trimesh.Trimesh( 

1545 ... vertices=[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], 

1546 ... faces=[[0, 1, 2], [0, 2, 3]], 

1547 ... process=False, 

1548 ... ) 

1549 >>> edges = np.sort([[2, 0], [0, 1]], axis=1) 

1550 >>> distance, index = mesh.face_adjacency_edges_tree.query(edges) 

1551 >>> matched = distance < 1e-12 

1552 >>> mesh.face_adjacency[index[matched]] 

1553 array([[0, 1]]) 

1554 """ 

1555 return cKDTree(self.face_adjacency_edges) 

1556 

1557 @cache_decorator 

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

1559 """ 

1560 Return the unsigned angle between adjacent faces 

1561 in radians. 

1562 

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

1564 use the `face_adjacency_convex` attribute to get a 

1565 signed angle with advanced indexing: 

1566 

1567 ``` 

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

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

1570 

1571 # apply the signs to the angles 

1572 angles = mesh.face_adjacency_angles * signs 

1573 ``` 

1574 

1575 Returns 

1576 -------- 

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

1578 Unsigned angle between adjacent faces 

1579 corresponding with `self.face_adjacency` 

1580 """ 

1581 # get pairs of unit vectors for adjacent faces 

1582 pairs = self.face_normals[self.face_adjacency] 

1583 # find the angle between the pairs of vectors 

1584 angles = geometry.vector_angle(pairs) 

1585 return angles 

1586 

1587 @cache_decorator 

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

1589 """ 

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

1591 its adjacent face 

1592 

1593 Returns 

1594 ---------- 

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

1596 Dot product of vertex 

1597 onto plane of adjacent triangle. 

1598 """ 

1599 projections = convex.adjacency_projections(self) 

1600 return projections 

1601 

1602 @cache_decorator 

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

1604 """ 

1605 Return faces which are adjacent and locally convex. 

1606 

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

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

1609 has a projection that is zero or negative. 

1610 

1611 Returns 

1612 ---------- 

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

1614 Face pairs that are locally convex 

1615 """ 

1616 return self.face_adjacency_projections < tol.merge 

1617 

1618 @cache_decorator 

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

1620 """ 

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

1622 edge between two adjacent faces 

1623 

1624 Returns 

1625 ----------- 

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

1627 Indexes of mesh.vertices 

1628 """ 

1629 return graph.face_adjacency_unshared(self) 

1630 

1631 @cache_decorator 

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

1633 """ 

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

1635 

1636 Returns 

1637 ------------ 

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

1639 Approximate radius formed by triangle pair 

1640 """ 

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

1642 return radii 

1643 

1644 @cache_decorator 

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

1646 """ 

1647 The approximate perpendicular projection of the non-shared 

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

1649 the two faces. 

1650 

1651 Returns 

1652 ------------ 

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

1654 Approximate span between the non-shared vertices 

1655 """ 

1656 _ = self.face_adjacency_radius 

1657 return self._cache["face_adjacency_span"] 

1658 

1659 @cache_decorator 

1660 def integral_mean_curvature(self) -> float64: 

1661 """ 

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

1663 

1664 Returns 

1665 --------- 

1666 area : float 

1667 Integral mean curvature of mesh 

1668 """ 

1669 edges_length = np.linalg.norm( 

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

1671 ) 

1672 # assign signs based on convex adjacency of face pairs 

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

1674 # adjust face adjacency angles with signs to reflect orientation 

1675 angles = self.face_adjacency_angles * signs 

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

1677 

1678 @cache_decorator 

1679 def vertex_adjacency_graph(self) -> Graph: 

1680 """ 

1681 Returns a networkx graph representing the vertices and their connections 

1682 in the mesh. 

1683 

1684 Returns 

1685 --------- 

1686 graph: networkx.Graph 

1687 Graph representing vertices and edges between 

1688 them where vertices are nodes and edges are edges 

1689 

1690 Examples 

1691 ---------- 

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

1693 potentially for some simple smoothing techniques. 

1694 

1695 mesh = trimesh.primitives.Box() 

1696 graph = mesh.vertex_adjacency_graph 

1697 graph.neighbors(0) 

1698 > [1, 2, 3, 4] 

1699 """ 

1700 

1701 return graph.vertex_adjacency_graph(mesh=self) 

1702 

1703 @cache_decorator 

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

1705 """ 

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

1707 the cached vertex_adjacency_graph, if already existent. 

1708 

1709 Returns 

1710 ---------- 

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

1712 Represents immediate neighbors of each vertex along 

1713 the edge of a triangle 

1714 

1715 Examples 

1716 ---------- 

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

1718 potentially for some simple smoothing techniques. 

1719 

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

1721 >>> mesh.vertex_neighbors[0] 

1722 [1, 2, 3, 4] 

1723 """ 

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

1725 

1726 @cache_decorator 

1727 def is_winding_consistent(self) -> bool: 

1728 """ 

1729 Does the mesh have consistent winding or not. 

1730 A mesh with consistent winding has each shared edge 

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

1732 

1733 Returns 

1734 -------- 

1735 consistent : bool 

1736 Is winding is consistent or not 

1737 """ 

1738 if self.is_empty: 

1739 return False 

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

1741 _ = self.is_watertight 

1742 return self._cache["is_winding_consistent"] 

1743 

1744 @cache_decorator 

1745 def is_watertight(self) -> bool: 

1746 """ 

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

1748 included in two faces. 

1749 

1750 Returns 

1751 ---------- 

1752 is_watertight : bool 

1753 Is mesh watertight or not 

1754 """ 

1755 if self.is_empty: 

1756 return False 

1757 watertight, winding = graph.is_watertight( 

1758 edges=self.edges, edges_sorted=self.edges_sorted 

1759 ) 

1760 self._cache["is_winding_consistent"] = winding 

1761 return watertight 

1762 

1763 @cache_decorator 

1764 def is_volume(self) -> bool: 

1765 """ 

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

1767 a valid volume, rather than just a surface. 

1768 

1769 These properties include being watertight, having consistent 

1770 winding and outward facing normals. 

1771 

1772 Returns 

1773 --------- 

1774 valid 

1775 Does the mesh represent a volume 

1776 """ 

1777 return bool( 

1778 self.is_watertight 

1779 and self.is_winding_consistent 

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

1781 and self.volume > 0.0 

1782 ) 

1783 

1784 @property 

1785 def is_empty(self) -> bool: 

1786 """ 

1787 Does the current mesh have data defined. 

1788 

1789 Returns 

1790 -------- 

1791 empty : bool 

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

1793 """ 

1794 return self._data.is_empty() 

1795 

1796 @cache_decorator 

1797 def is_convex(self) -> bool: 

1798 """ 

1799 Check if a mesh is convex or not. 

1800 

1801 Returns 

1802 ---------- 

1803 is_convex: bool 

1804 Is mesh convex or not 

1805 """ 

1806 if self.is_empty: 

1807 return False 

1808 

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

1810 return is_convex 

1811 

1812 @cache_decorator 

1813 def kdtree(self) -> cKDTree: 

1814 """ 

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

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

1817 

1818 Returns 

1819 --------- 

1820 tree : scipy.spatial.cKDTree 

1821 Contains mesh.vertices 

1822 """ 

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

1824 

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

1826 """ 

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

1828 in the current mesh. 

1829 

1830 Usage example for removing them: 

1831 `mesh.update_faces(mesh.nondegenerate_faces())` 

1832 

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

1834 bounding box with one edge shorter than that height. 

1835 

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

1837 

1838 Parameters 

1839 ------------ 

1840 height : float 

1841 If specified identifies faces with an oriented bounding 

1842 box shorter than this on one side. 

1843 

1844 Returns 

1845 ------------- 

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

1847 Mask that can be used to remove faces 

1848 """ 

1849 return triangles.nondegenerate( 

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

1851 ) 

1852 

1853 @cache_decorator 

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

1855 """ 

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

1857 

1858 Returns 

1859 --------- 

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

1861 Groups of indexes of self.faces 

1862 """ 

1863 facets = graph.facets(self) 

1864 return facets 

1865 

1866 @cache_decorator 

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

1868 """ 

1869 Return an array containing the area of each facet. 

1870 

1871 Returns 

1872 --------- 

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

1874 Total area of each facet (group of faces) 

1875 """ 

1876 # avoid thrashing the cache inside a loop 

1877 area_faces = self.area_faces 

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

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

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

1881 # native sum provides roughly a 50% speedup 

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

1883 return areas 

1884 

1885 @cache_decorator 

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

1887 """ 

1888 Return the normal of each facet 

1889 

1890 Returns 

1891 --------- 

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

1893 A unit normal vector for each facet 

1894 """ 

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

1896 return np.array([]) 

1897 

1898 area_faces = self.area_faces 

1899 

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

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

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

1903 normals = self.face_normals[index] 

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

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

1906 # save origins in cache 

1907 self._cache["facets_origin"] = origins 

1908 

1909 return normals 

1910 

1911 @cache_decorator 

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

1913 """ 

1914 Return a point on the facet plane. 

1915 

1916 Returns 

1917 ------------ 

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

1919 A point on each facet plane 

1920 """ 

1921 _ = self.facets_normal 

1922 return self._cache["facets_origin"] 

1923 

1924 @cache_decorator 

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

1926 """ 

1927 Return the edges which represent the boundary of each facet 

1928 

1929 Returns 

1930 --------- 

1931 edges_boundary : sequence of (n, 2) int 

1932 Indices of self.vertices 

1933 """ 

1934 # make each row correspond to a single face 

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

1936 # get the edges for each facet 

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

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

1939 return edges_boundary 

1940 

1941 @cache_decorator 

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

1943 """ 

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

1945 

1946 Returns 

1947 --------- 

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

1949 is A facet on the meshes convex hull or not 

1950 """ 

1951 # if no facets exit early 

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

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

1954 

1955 # facets plane, origin and normal 

1956 normals = self.facets_normal 

1957 origins = self.facets_origin 

1958 

1959 # (n, 3) convex hull vertices 

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

1961 

1962 # boolean mask for which facets are on convex hull 

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

1964 

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

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

1967 # of the convex hull is behind that plane 

1968 # which we are checking with dot products 

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

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

1971 

1972 return on_hull 

1973 

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

1975 """ 

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

1977 winding direction. 

1978 

1979 For face normals ensure that vectors are consistently pointed 

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

1981 for all connected components. 

1982 

1983 Parameters 

1984 ------------- 

1985 multibody : None or bool 

1986 Fix normals across multiple bodies or if unspecified 

1987 check the current `Trimesh.body_count`. 

1988 """ 

1989 if multibody is None: 

1990 multibody = self.body_count > 1 

1991 repair.fix_normals(self, multibody=multibody) 

1992 return self 

1993 

1994 def fill_holes(self) -> bool: 

1995 """ 

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

1997 

1998 Returns 

1999 ---------- 

2000 watertight : bool 

2001 Is the mesh watertight after the function completes 

2002 """ 

2003 return repair.fill_holes(self) 

2004 

2005 def register( 

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

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

2008 """ 

2009 Align a mesh with another mesh or a PointCloud using 

2010 the principal axes of inertia as a starting point which 

2011 is refined by iterative closest point. 

2012 

2013 Parameters 

2014 ------------ 

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

2016 Mesh or points in space 

2017 samples : int 

2018 Number of samples from mesh surface to align 

2019 icp_first : int 

2020 How many ICP iterations for the 9 possible 

2021 combinations of 

2022 icp_final : int 

2023 How many ICP itertations for the closest 

2024 candidate from the wider search 

2025 

2026 Returns 

2027 ----------- 

2028 mesh_to_other : (4, 4) float 

2029 Transform to align mesh to the other object 

2030 cost : float 

2031 Average square distance per point 

2032 """ 

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

2034 return mesh_to_other, cost 

2035 

2036 def compute_stable_poses( 

2037 self, 

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

2039 sigma: Floating = 0.0, 

2040 n_samples: Integer = 1, 

2041 threshold: Floating = 0.0, 

2042 seed: Seed = None, 

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

2044 """ 

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

2046 

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

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

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

2050 a planar workspace and evaluates the probabilities of landing in 

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

2052 

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

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

2055 and a list of the probabilities for each pose. 

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

2057 most probable pose first. 

2058 

2059 Parameters 

2060 ------------ 

2061 center_mass : (3, ) float 

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

2063 assumes uniform density and watertightness and 

2064 computes a center of mass explicitly) 

2065 sigma : float 

2066 The covariance for the multivariate gaussian used 

2067 to sample center of mass locations 

2068 n_samples : int 

2069 The number of samples of the center of mass location 

2070 threshold : float 

2071 The probability value at which to threshold 

2072 returned stable poses 

2073 

2074 Returns 

2075 ------- 

2076 transforms : (n, 4, 4) float 

2077 The homogeneous matrices that transform the 

2078 object to rest in a stable pose, with the 

2079 new z-axis pointing upwards from the table 

2080 and the object just touching the table. 

2081 

2082 probs : (n, ) float 

2083 A probability ranging from 0.0 to 1.0 for each pose 

2084 """ 

2085 return poses.compute_stable_poses( 

2086 mesh=self, 

2087 center_mass=center_mass, 

2088 sigma=sigma, 

2089 n_samples=n_samples, 

2090 threshold=threshold, 

2091 seed=seed, 

2092 ) 

2093 

2094 def subdivide( 

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

2096 ) -> "Trimesh": 

2097 """ 

2098 Subdivide a mesh with each subdivided face replaced 

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

2100 mesh with subdivided faces. 

2101 

2102 Parameters 

2103 ------------ 

2104 face_index : (m, ) int or None 

2105 If None all faces of mesh will be subdivided 

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

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

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

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

2110 and an additional postprocessing step will be required to 

2111 make resulting mesh watertight 

2112 iterations : int 

2113 If passed will run subdivisions multiple times recursively. 

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

2115 if both arguments are passed. 

2116 

2117 Returns 

2118 ------------ 

2119 mesh: trimesh.Trimesh 

2120 The copy of current mesh with subdivided faces. 

2121 """ 

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

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

2124 

2125 visual = None 

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

2127 len(self.vertices), 

2128 2, 

2129 ): 

2130 # uv coords divided along with vertices 

2131 vertices, faces, attr = remesh.subdivide( 

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

2133 faces=self.faces, 

2134 face_index=face_index, 

2135 vertex_attributes=self.vertex_attributes, 

2136 ) 

2137 

2138 # get a copy of the current visuals 

2139 visual = self.visual.copy() 

2140 

2141 # separate uv coords and vertices 

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

2143 

2144 else: 

2145 # perform the subdivision with vertex attributes 

2146 vertices, faces, attr = remesh.subdivide( 

2147 vertices=self.vertices, 

2148 faces=self.faces, 

2149 face_index=face_index, 

2150 vertex_attributes=self.vertex_attributes, 

2151 ) 

2152 

2153 # create a new mesh 

2154 result = Trimesh( 

2155 vertices=vertices, 

2156 faces=faces, 

2157 visual=visual, 

2158 vertex_attributes=attr, 

2159 process=False, 

2160 ) 

2161 

2162 if iterations is not None and iterations > 1: 

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

2164 

2165 return result 

2166 

2167 def subdivide_to_size( 

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

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

2170 """ 

2171 Subdivide a mesh until every edge is shorter than a 

2172 specified length. 

2173 

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

2175 

2176 Parameters 

2177 ------------ 

2178 max_edge 

2179 Maximum length of any edge in the result 

2180 max_iter : int 

2181 The maximum number of times to run subdivision 

2182 return_index : bool 

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

2184 

2185 Returns 

2186 ------------ 

2187 mesh: trimesh.Trimesh 

2188 The copy of current mesh with subdivided faces. 

2189 """ 

2190 # subdivide vertex attributes 

2191 visual = None 

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

2193 len(self.vertices), 

2194 2, 

2195 ): 

2196 # uv coords divided along with vertices 

2197 vertices_faces = remesh.subdivide_to_size( 

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

2199 faces=self.faces, 

2200 max_edge=max_edge, 

2201 max_iter=max_iter, 

2202 return_index=return_index, 

2203 ) 

2204 # unpack result 

2205 if return_index: 

2206 vertices, faces, final_index = vertices_faces 

2207 else: 

2208 vertices, faces = vertices_faces 

2209 

2210 # get a copy of the current visuals 

2211 visual = self.visual.copy() 

2212 

2213 # separate uv coords and vertices 

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

2215 

2216 else: 

2217 # uv coords divided along with vertices 

2218 vertices_faces = remesh.subdivide_to_size( 

2219 vertices=self.vertices, 

2220 faces=self.faces, 

2221 max_edge=max_edge, 

2222 max_iter=max_iter, 

2223 return_index=return_index, 

2224 ) 

2225 # unpack result 

2226 if return_index: 

2227 vertices, faces, final_index = vertices_faces 

2228 else: 

2229 vertices, faces = vertices_faces 

2230 

2231 # create a new mesh 

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

2233 

2234 if return_index: 

2235 return result, final_index 

2236 

2237 return result 

2238 

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

2240 """ 

2241 Subdivide a mesh by dividing each triangle into four 

2242 triangles and approximating their smoothed surface 

2243 using loop subdivision. Loop subdivision often looks 

2244 better on triangular meshes than catmul-clark, which 

2245 operates primarily on quads. 

2246 

2247 Parameters 

2248 ------------ 

2249 iterations : int 

2250 Number of iterations to run subdivision. 

2251 multibody : bool 

2252 If True will try to subdivide for each submesh 

2253 

2254 Returns 

2255 ------------ 

2256 mesh: trimesh.Trimesh 

2257 The copy of current mesh with subdivided faces. 

2258 """ 

2259 # perform subdivision for one mesh 

2260 new_vertices, new_faces = remesh.subdivide_loop( 

2261 vertices=self.vertices, faces=self.faces, iterations=iterations 

2262 ) 

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

2264 

2265 @property 

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

2267 """ 

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

2269 this function will disconnect regions above an angle threshold 

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

2271 in an OpenGL rendering context. 

2272 

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

2274 

2275 Returns 

2276 --------- 

2277 smooth_shaded : trimesh.Trimesh 

2278 Non watertight version of current mesh. 

2279 """ 

2280 # key this also by the visual properties 

2281 # but store it in the mesh cache 

2282 self.visual._verify_hash() 

2283 cache = self.visual._cache 

2284 # needs to be dumped whenever visual or mesh changes 

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

2286 if key in cache: 

2287 return cache[key] 

2288 smooth = graph.smooth_shade(self) 

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

2290 cache[key] = smooth 

2291 return smooth 

2292 

2293 @property 

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

2295 """ 

2296 Get the stored visuals for the current mesh. 

2297 

2298 Returns 

2299 ------------- 

2300 visual : ColorVisuals or TextureVisuals 

2301 Contains visual information about the mesh 

2302 """ 

2303 if hasattr(self, "_visual"): 

2304 return self._visual 

2305 return None 

2306 

2307 @visual.setter 

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

2309 """ 

2310 When setting a visual object, always make sure 

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

2312 

2313 Parameters 

2314 -------------- 

2315 visual : ColorVisuals or TextureVisuals 

2316 Contains visual information about the mesh 

2317 """ 

2318 if value is None: 

2319 value = ColorVisuals() 

2320 value.mesh = self 

2321 self._visual = value 

2322 

2323 def section( 

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

2325 ) -> Path3D | None: 

2326 """ 

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

2328 defined by origin and normal. 

2329 

2330 Parameters 

2331 ------------ 

2332 plane_normal : (3,) float 

2333 Normal vector of section plane. 

2334 plane_origin : (3, ) float 

2335 Point on the cross section plane. 

2336 

2337 Returns 

2338 --------- 

2339 intersections 

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

2341 """ 

2342 # turn line segments into Path2D/Path3D objects 

2343 from .path.exchange.misc import lines_to_path 

2344 from .path.path import Path3D 

2345 

2346 # return a single cross section in 3D 

2347 lines, _face_index = intersections.mesh_plane( 

2348 mesh=self, 

2349 plane_normal=plane_normal, 

2350 plane_origin=plane_origin, 

2351 return_faces=True, 

2352 **kwargs, 

2353 ) 

2354 

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

2356 if len(lines) == 0: 

2357 return None 

2358 

2359 # otherwise load the line segments into the keyword arguments 

2360 # for a Path3D object. 

2361 path = lines_to_path(lines) 

2362 

2363 # add the face index info into metadata 

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

2365 

2366 return Path3D(**path) 

2367 

2368 def section_multiplane( 

2369 self, 

2370 plane_origin: ArrayLike, 

2371 plane_normal: ArrayLike, 

2372 heights: ArrayLike, 

2373 ) -> list[Path2D | None]: 

2374 """ 

2375 Return multiple parallel cross sections of the current 

2376 mesh in 2D. 

2377 

2378 Parameters 

2379 ------------ 

2380 plane_origin : (3, ) float 

2381 Point on the cross section plane 

2382 plane_normal : (3) float 

2383 Normal vector of section plane 

2384 heights : (n, ) float 

2385 Each section is offset by height along 

2386 the plane normal. 

2387 

2388 Returns 

2389 --------- 

2390 paths : (n, ) Path2D or None 

2391 2D cross sections at specified heights. 

2392 path.metadata['to_3D'] contains transform 

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

2394 """ 

2395 # turn line segments into Path2D/Path3D objects 

2396 from .exchange.load import load_path 

2397 

2398 # do a multiplane intersection 

2399 lines, transforms, faces = intersections.mesh_multiplane( 

2400 mesh=self, 

2401 plane_normal=plane_normal, 

2402 plane_origin=plane_origin, 

2403 heights=heights, 

2404 ) 

2405 

2406 # turn the line segments into Path2D objects 

2407 paths = [None] * len(lines) 

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

2409 if len(segments) > 0: 

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

2411 return paths 

2412 

2413 def slice_plane( 

2414 self, 

2415 plane_origin: ArrayLike, 

2416 plane_normal: ArrayLike, 

2417 cap: bool = False, 

2418 face_index: ArrayLike | None = None, 

2419 **kwargs, 

2420 ) -> "Trimesh": 

2421 """ 

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

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

2424 

2425 plane_origin : (3,) float 

2426 Point on plane to intersect with mesh 

2427 plane_normal : (3,) float 

2428 Normal vector of plane to intersect with mesh 

2429 cap : bool 

2430 If True, cap the result with a triangulated polygon 

2431 face_index : ((m,) int) 

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

2433 provided, the default is to slice all faces. 

2434 

2435 Returns 

2436 --------- 

2437 new_mesh: trimesh.Trimesh or None 

2438 Subset of current mesh that intersects the half plane 

2439 to the positive normal side of the plane 

2440 """ 

2441 

2442 # return a new mesh 

2443 new_mesh = intersections.slice_mesh_plane( 

2444 mesh=self, 

2445 plane_normal=plane_normal, 

2446 plane_origin=plane_origin, 

2447 cap=cap, 

2448 face_index=face_index, 

2449 **kwargs, 

2450 ) 

2451 

2452 return new_mesh 

2453 

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

2455 """ 

2456 Returns a Trimesh object equivalent to the current mesh where 

2457 the vertices have been assigned uv texture coordinates. Vertices 

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

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

2460 

2461 Requires `pip install xatlas` 

2462 

2463 Parameters 

2464 ------------ 

2465 image : None or PIL.Image 

2466 Image to assign to the material 

2467 

2468 Returns 

2469 -------- 

2470 unwrapped : trimesh.Trimesh 

2471 Mesh with unwrapped uv coordinates 

2472 """ 

2473 import xatlas 

2474 

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

2476 

2477 result = Trimesh( 

2478 vertices=self.vertices[vmap], 

2479 faces=faces, 

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

2481 process=False, 

2482 ) 

2483 

2484 # run additional checks for unwrapping 

2485 if tol.strict: 

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

2487 # move the indices around on creation 

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

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

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

2491 # check to make sure indices are still the 

2492 # same order after we've exported to OBJ 

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

2494 uv_recon = np.array( 

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

2496 dtype=float64, 

2497 ) 

2498 assert np.allclose(uv_recon, uv) 

2499 v_recon = np.array( 

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

2501 dtype=float64, 

2502 ) 

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

2504 

2505 return result 

2506 

2507 @cache_decorator 

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

2509 """ 

2510 Returns a Trimesh object representing the convex hull of 

2511 the current mesh. 

2512 

2513 Returns 

2514 -------- 

2515 convex : trimesh.Trimesh 

2516 Mesh of convex hull of current mesh 

2517 """ 

2518 return convex.convex_hull(self) 

2519 

2520 def sample( 

2521 self, 

2522 count: Integer, 

2523 return_index: bool = False, 

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

2525 return_barycentric: bool = False, 

2526 seed: Seed = None, 

2527 ): 

2528 """ 

2529 Return random samples distributed across the 

2530 surface of the mesh 

2531 

2532 Parameters 

2533 ------------ 

2534 count : int 

2535 Number of points to sample 

2536 return_index : bool 

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

2538 sample was taken from. 

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

2540 Weight faces by a factor other than face area. 

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

2542 return_barycentric : bool 

2543 If True will also return the barycentric coordinates 

2544 of each sampled point. 

2545 seed : None or int 

2546 Seed for deterministic results, otherwise OS entropy. 

2547 

2548 Returns 

2549 --------- 

2550 samples : (count, 3) float 

2551 Points on surface of mesh 

2552 face_index : (count,) int 

2553 Index of self.faces 

2554 Returned only when return_index is True 

2555 barycentric : (count, 3) float 

2556 Coordinates on `self.faces[face_index]` 

2557 Returned only when return_barycentric is True 

2558 """ 

2559 result = sample.sample_surface( 

2560 mesh=self, 

2561 count=count, 

2562 face_weight=face_weight, 

2563 return_barycentric=return_barycentric, 

2564 seed=seed, 

2565 ) 

2566 if return_index: 

2567 return result 

2568 

2569 # `sample_surface` always returns the face index we weren't asked for 

2570 if return_barycentric: 

2571 return result[0], result[2] 

2572 

2573 return result[0] 

2574 

2575 def remove_unreferenced_vertices(self) -> None: 

2576 """ 

2577 Remove all vertices in the current mesh which are not 

2578 referenced by a face. 

2579 """ 

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

2581 referenced[self.faces] = True 

2582 

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

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

2585 

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

2587 

2588 def unmerge_vertices(self) -> None: 

2589 """ 

2590 Removes all face references so that every face contains 

2591 three unique vertex indices and no faces are adjacent. 

2592 """ 

2593 # new faces are incrementing so every vertex is unique 

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

2595 

2596 # use update_vertices to apply mask to 

2597 # all properties that are per-vertex 

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

2599 # set faces to incrementing indexes 

2600 self.faces = faces 

2601 # keep face normals as the haven't changed 

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

2603 

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

2605 """ 

2606 Transform mesh by a homogeneous transformation matrix. 

2607 

2608 Does the bookkeeping to avoid recomputing things so this function 

2609 should be used rather than directly modifying self.vertices 

2610 if possible. 

2611 

2612 Parameters 

2613 ------------ 

2614 matrix : (4, 4) float 

2615 Homogeneous transformation matrix 

2616 """ 

2617 # get c-order float64 matrix 

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

2619 

2620 # only support homogeneous transformations 

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

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

2623 

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

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

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

2627 return self 

2628 

2629 # new vertex positions 

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

2631 

2632 # check to see if the matrix has rotation 

2633 # rather than just translation 

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

2635 # check to see if the matrix flips the winding of triangles 

2636 flip = has_rotation and transformations.flips_winding(matrix) 

2637 

2638 # transform overridden center of mass 

2639 if "center_mass" in self._data: 

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

2641 self.center_mass = transformations.transform_points( 

2642 center_mass, 

2643 matrix, 

2644 )[0] 

2645 

2646 if has_rotation: 

2647 linear = matrix[:3, :3] 

2648 # the adjugate `det(M) * inv(M)` maps `cross(a, b)` to 

2649 # `cross(M @ a, M @ b)` exactly, even for singular `M` 

2650 adjugate = np.cross(linear.T[[1, 2, 0]], linear.T[[2, 0, 1]]) 

2651 if flip: 

2652 # faces are reversed below so their normals reverse too 

2653 adjugate = -adjugate 

2654 if "face_normals" in self._cache: 

2655 self._cache["face_normals"] = util.unitize( 

2656 np.dot(self.face_normals, adjugate), threshold=0 

2657 ) 

2658 if "vertex_normals" in self._cache: 

2659 self._cache["vertex_normals"] = util.unitize( 

2660 np.dot(self.vertex_normals, adjugate), threshold=0 

2661 ) 

2662 

2663 # if transformation flips winding of triangles 

2664 if flip: 

2665 log.debug("transform flips winding") 

2666 # fliplr will make array non C contiguous 

2667 # which will cause hashes to be more 

2668 # expensive than necessary so wrap 

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

2670 

2671 # assign the new values 

2672 self.vertices = new_vertices 

2673 

2674 # preserve normals and topology in cache 

2675 # while dumping everything else 

2676 self._cache.clear( 

2677 exclude={ 

2678 "face_normals", # transformed by us 

2679 "vertex_normals", # also transformed by us 

2680 "face_adjacency", # topological 

2681 "face_adjacency_edges", 

2682 "face_adjacency_unshared", 

2683 "edges", 

2684 "edges_face", 

2685 "edges_sorted", 

2686 "edges_unique", 

2687 "edges_unique_idx", 

2688 "edges_unique_inverse", 

2689 "edges_sparse", 

2690 "body_count", 

2691 "faces_unique_edges", 

2692 "euler_number", 

2693 } 

2694 ) 

2695 # set the cache ID with the current hash value 

2696 self._cache.id_set() 

2697 return self 

2698 

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

2700 """ 

2701 Return a VoxelGrid object representing the current mesh 

2702 discretized into voxels at the specified pitch 

2703 

2704 Parameters 

2705 ------------ 

2706 pitch : float 

2707 The edge length of a single voxel 

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

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

2710 

2711 Returns 

2712 ---------- 

2713 voxelized : VoxelGrid object 

2714 Representing the current mesh 

2715 """ 

2716 from .voxel import creation 

2717 

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

2719 

2720 def simplify_quadric_decimation( 

2721 self, 

2722 percent: Floating | None = None, 

2723 face_count: Integer | None = None, 

2724 aggression: Integer | None = None, 

2725 ) -> "Trimesh": 

2726 """ 

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

2728 

2729 Parameters 

2730 ----------- 

2731 percent 

2732 A number between 0.0 and 1.0 for how much of the mesh to 

2733 remove, i.e. `percent=0.9` keeps roughly 10% of the faces. 

2734 Passed to `fast_simplification` as `target_reduction`. 

2735 face_count 

2736 Target number of faces desired in the resulting mesh. 

2737 Pass either `percent` or `face_count`, not both. 

2738 aggression 

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

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

2741 

2742 Returns 

2743 --------- 

2744 simple : trimesh.Trimesh 

2745 Simplified version of mesh. 

2746 """ 

2747 from fast_simplification import simplify 

2748 

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

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

2751 # and requires they be omitted from the constructor 

2752 kwargs = { 

2753 "target_count": face_count, 

2754 "target_reduction": percent, 

2755 "agg": aggression, 

2756 } 

2757 

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

2759 # apply the same simplification to the visual info 

2760 vertices, faces = simplify( 

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

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

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

2764 ) 

2765 

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

2767 

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

2769 """ 

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

2771 faces and return it as a Path3D. 

2772 

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

2774 included by a single triangle. 

2775 

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

2777 outline of a watertight mesh is an empty path. 

2778 

2779 Parameters 

2780 ------------ 

2781 face_ids : (n, ) int 

2782 Indices to compute the outline of. 

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

2784 **kwargs: passed to Path3D constructor 

2785 

2786 Returns 

2787 ---------- 

2788 path : Path3D 

2789 Curve in 3D of the outline 

2790 """ 

2791 from .path.exchange.misc import faces_to_path 

2792 

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

2794 

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

2796 """ 

2797 Project a mesh onto a plane and then extract the 

2798 polygon that outlines the mesh projection on that 

2799 plane. 

2800 

2801 Parameters 

2802 ---------- 

2803 normal : (3,) float 

2804 Normal to extract flat pattern along 

2805 origin : None or (3,) float 

2806 Origin of plane to project mesh onto 

2807 ignore_sign : bool 

2808 Allow a projection from the normal vector in 

2809 either direction: this provides a substantial speedup 

2810 on watertight meshes where the direction is irrelevant 

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

2812 backfaces you should set this to False. 

2813 rpad : float 

2814 Proportion to pad polygons by before unioning 

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

2816 apad : float 

2817 Absolute padding to pad polygons by before unioning 

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

2819 tol_dot : float 

2820 Tolerance for discarding on-edge triangles. 

2821 precise : bool 

2822 Use the precise projection computation using shapely. 

2823 precise_eps : float 

2824 Tolerance for precise triangle checks. 

2825 

2826 Returns 

2827 ---------- 

2828 projected : trimesh.path.Path2D 

2829 Outline of source mesh 

2830 """ 

2831 from .exchange.load import load_path 

2832 from .path import Path2D 

2833 from .path.polygons import projected 

2834 

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

2836 if projection is None: 

2837 return Path2D() 

2838 return load_path(projection) 

2839 

2840 @cache_decorator 

2841 def area(self) -> float64: 

2842 """ 

2843 Summed area of all triangles in the current mesh. 

2844 

2845 Returns 

2846 --------- 

2847 area : float 

2848 Surface area of mesh 

2849 """ 

2850 area = self.area_faces.sum() 

2851 return area 

2852 

2853 @cache_decorator 

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

2855 """ 

2856 The area of each face in the mesh. 

2857 

2858 Returns 

2859 --------- 

2860 area_faces : (n, ) float 

2861 Area of each face 

2862 """ 

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

2864 

2865 @cache_decorator 

2866 def mass_properties(self) -> MassProperties: 

2867 """ 

2868 Returns the mass properties of the current mesh. 

2869 

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

2871 isn't watertight. 

2872 

2873 Returns 

2874 ---------- 

2875 properties : dict 

2876 With keys: 

2877 'volume' : in global units^3 

2878 'mass' : From specified density 

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

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

2881 coordinate system 

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

2883 """ 

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

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

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

2887 return triangles.mass_properties( 

2888 triangles=self.triangles, 

2889 crosses=self.triangles_cross, 

2890 density=density, 

2891 center_mass=center_mass, 

2892 skip_inertia=False, 

2893 ) 

2894 

2895 def invert(self) -> Self: 

2896 """ 

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

2898 face and negating normals without dumping the cache. 

2899 

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

2901 `self.face_normals` and `self.vertex_normals`. 

2902 """ 

2903 with self._cache: 

2904 if "face_normals" in self._cache: 

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

2906 if "vertex_normals" in self._cache: 

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

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

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

2910 # save our normals 

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

2912 

2913 return self 

2914 

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

2916 """ 

2917 Returns a Scene object containing the current mesh. 

2918 

2919 Returns 

2920 --------- 

2921 scene : trimesh.scene.scene.Scene 

2922 Contains just the current mesh 

2923 """ 

2924 return Scene(self, **kwargs) 

2925 

2926 def show( 

2927 self, 

2928 viewer: ViewerType = None, 

2929 **kwargs, 

2930 ) -> Scene: 

2931 """ 

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

2933 

2934 Parameters 

2935 ------------ 

2936 viewer : ViewerType 

2937 What kind of viewer to use, such as 

2938 `gl` to open a pyglet window 

2939 `jupyter` for a jupyter notebook 

2940 `marimo'` for a marimo notebook 

2941 None for a "best guess" 

2942 smooth : bool 

2943 Run smooth shading on mesh or not, 

2944 large meshes will be slow 

2945 

2946 Returns 

2947 ----------- 

2948 scene : trimesh.scene.Scene 

2949 Scene with current mesh in it 

2950 """ 

2951 scene = self.scene() 

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

2953 

2954 def submesh( 

2955 self, 

2956 faces_sequence: Sequence[ArrayLike], 

2957 only_watertight: bool = False, 

2958 repair: bool = False, 

2959 **kwargs, 

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

2961 """ 

2962 Return a subset of the mesh. 

2963 

2964 Parameters 

2965 ------------ 

2966 faces_sequence : sequence (m, ) int 

2967 Face indices of mesh 

2968 only_watertight : bool 

2969 Only return submeshes which are watertight 

2970 repair 

2971 Try to repair the submesh if it is not watertight 

2972 append : bool 

2973 Return a single mesh which has the faces appended. 

2974 if this flag is set, only_watertight is ignored 

2975 

2976 Returns 

2977 --------- 

2978 submesh : Trimesh or (n,) Trimesh 

2979 Single mesh if `append` or list of submeshes 

2980 """ 

2981 return util.submesh( 

2982 mesh=self, 

2983 faces_sequence=faces_sequence, 

2984 only_watertight=only_watertight, 

2985 repair=repair, 

2986 **kwargs, 

2987 ) 

2988 

2989 @cache_decorator 

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

2991 """ 

2992 Return a float vector which is unique to the mesh 

2993 and is robust to rotation and translation. 

2994 

2995 Returns 

2996 ----------- 

2997 identifier : (7,) float 

2998 Identifying properties of the current mesh 

2999 """ 

3000 return comparison.identifier_simple(self) 

3001 

3002 @cache_decorator 

3003 def identifier_hash(self) -> str: 

3004 """ 

3005 A hash of the rotation invariant identifier vector. 

3006 

3007 Returns 

3008 --------- 

3009 hashed : str 

3010 Hex string of the SHA256 hash from 

3011 the identifier vector at hand-tuned sigfigs. 

3012 """ 

3013 return comparison.identifier_hash(self.identifier) 

3014 

3015 def export( 

3016 self, 

3017 file_obj: Loadable = None, 

3018 file_type: str | None = None, 

3019 **kwargs, 

3020 ) -> dict | bytes | str: 

3021 """ 

3022 Export the current mesh to a file object. 

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

3024 

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

3026 dict, glb, dict64, msgpack. 

3027 

3028 Parameters 

3029 ------------ 

3030 file_obj : open writeable file object 

3031 str, file name where to save the mesh 

3032 None, return the export blob 

3033 file_type : str 

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

3035 is passed this is not required. 

3036 

3037 Returns 

3038 ---------- 

3039 exported : bytes or str 

3040 Result of exporter 

3041 """ 

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

3043 

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

3045 """ 

3046 Return a dictionary representation of the current mesh 

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

3048 Trimesh constructor and matches the schema in: 

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

3050 

3051 Returns 

3052 ---------- 

3053 result : dict 

3054 Matches schema and Trimesh constructor. 

3055 """ 

3056 return { 

3057 "kind": "trimesh", 

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

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

3060 } 

3061 

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

3063 """ 

3064 Compute an approximate convex decomposition of a mesh 

3065 using `pip install pyVHACD`. 

3066 

3067 Returns 

3068 ------- 

3069 meshes 

3070 List of convex meshes that approximate the original 

3071 **kwargs : VHACD keyword arguments 

3072 """ 

3073 return [ 

3074 Trimesh(**kwargs) 

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

3076 ] 

3077 

3078 def union( 

3079 self, 

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

3081 engine: BooleanEngineType = None, 

3082 check_volume: bool = True, 

3083 **kwargs, 

3084 ) -> "Trimesh": 

3085 """ 

3086 Boolean union between this mesh and other meshes. 

3087 

3088 Parameters 

3089 ------------ 

3090 other : Trimesh or (n, ) Trimesh 

3091 Other meshes to union 

3092 engine 

3093 Which backend to use, the default 

3094 recommendation is: `pip install manifold3d`. 

3095 check_volume 

3096 Raise an error if not all meshes are watertight 

3097 positive volumes. Advanced users may want to ignore 

3098 this check as it is expensive. 

3099 kwargs 

3100 Passed through to the `engine`. 

3101 

3102 Returns 

3103 --------- 

3104 union : trimesh.Trimesh 

3105 Union of self and other Trimesh objects 

3106 """ 

3107 return boolean.union( 

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

3109 engine=engine, 

3110 check_volume=check_volume, 

3111 **kwargs, 

3112 ) 

3113 

3114 def difference( 

3115 self, 

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

3117 engine: BooleanEngineType = None, 

3118 check_volume: bool = True, 

3119 **kwargs, 

3120 ) -> "Trimesh": 

3121 """ 

3122 Boolean difference between this mesh and other meshes. 

3123 

3124 Parameters 

3125 ------------ 

3126 other 

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

3128 engine 

3129 Which backend to use, the default 

3130 recommendation is: `pip install manifold3d`. 

3131 check_volume 

3132 Raise an error if not all meshes are watertight 

3133 positive volumes. Advanced users may want to ignore 

3134 this check as it is expensive. 

3135 kwargs 

3136 Passed through to the `engine`. 

3137 

3138 Returns 

3139 --------- 

3140 difference : trimesh.Trimesh 

3141 Difference between self and other Trimesh objects 

3142 """ 

3143 return boolean.difference( 

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

3145 engine=engine, 

3146 check_volume=check_volume, 

3147 **kwargs, 

3148 ) 

3149 

3150 def intersection( 

3151 self, 

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

3153 engine: BooleanEngineType = None, 

3154 check_volume: bool = True, 

3155 **kwargs, 

3156 ) -> "Trimesh": 

3157 """ 

3158 Boolean intersection between this mesh and other meshes. 

3159 

3160 Parameters 

3161 ------------ 

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

3163 Meshes to calculate intersections with 

3164 engine 

3165 Which backend to use, the default 

3166 recommendation is: `pip install manifold3d`. 

3167 check_volume 

3168 Raise an error if not all meshes are watertight 

3169 positive volumes. Advanced users may want to ignore 

3170 this check as it is expensive. 

3171 kwargs 

3172 Passed through to the `engine`. 

3173 

3174 Returns 

3175 --------- 

3176 intersection : trimesh.Trimesh 

3177 Mesh of the volume contained by all passed meshes 

3178 """ 

3179 return boolean.intersection( 

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

3181 engine=engine, 

3182 check_volume=check_volume, 

3183 **kwargs, 

3184 ) 

3185 

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

3187 """ 

3188 Given an array of points determine whether or not they 

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

3190 non-watertight mesh. 

3191 

3192 Parameters 

3193 ------------ 

3194 points : (n, 3) float 

3195 Points in cartesian space 

3196 

3197 Returns 

3198 --------- 

3199 contains : (n, ) bool 

3200 Whether or not each point is inside the mesh 

3201 """ 

3202 return self.ray.contains_points(points) 

3203 

3204 @cache_decorator 

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

3206 """ 

3207 Returns the angle at each vertex of a face. 

3208 

3209 Returns 

3210 -------- 

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

3212 Angle at each vertex of a face 

3213 """ 

3214 return triangles.angles(self.triangles) 

3215 

3216 @cache_decorator 

3217 def face_angles_sparse(self) -> coo_matrix: 

3218 """ 

3219 A sparse matrix representation of the face angles. 

3220 

3221 Returns 

3222 ---------- 

3223 sparse : scipy.sparse.coo_matrix 

3224 Float sparse matrix with with shape: 

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

3226 """ 

3227 angles = curvature.face_angles_sparse(self) 

3228 return angles 

3229 

3230 @cache_decorator 

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

3232 """ 

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

3234 of every face that includes that vertex. 

3235 

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

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

3238 concave negative. 

3239 

3240 Returns 

3241 -------- 

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

3243 Vertex defect at the every vertex 

3244 """ 

3245 defects = curvature.vertex_defects(self) 

3246 return defects 

3247 

3248 @cache_decorator 

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

3250 """ 

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

3252 

3253 Returns 

3254 ---------- 

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

3256 Number of faces each vertex is included in 

3257 """ 

3258 # get degree through sparse matrix 

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

3260 return degree 

3261 

3262 @cache_decorator 

3263 def face_adjacency_tree(self) -> Index: 

3264 """ 

3265 An R-tree of face adjacencies. 

3266 

3267 Returns 

3268 -------- 

3269 tree 

3270 Where each edge in self.face_adjacency has a 

3271 rectangular cell 

3272 """ 

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

3274 return util.bounds_tree( 

3275 np.column_stack( 

3276 ( 

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

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

3279 ) 

3280 ) 

3281 ) 

3282 

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

3284 """ 

3285 Safely return a copy of the current mesh. 

3286 

3287 By default, copied meshes will have emptied cache 

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

3289 operations until caches are regenerated. 

3290 

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

3292 

3293 Parameters 

3294 ------------ 

3295 include_cache : bool 

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

3297 include_visual : bool 

3298 If True, will copy visual information 

3299 

3300 Returns 

3301 --------- 

3302 copied : trimesh.Trimesh 

3303 Copy of current mesh 

3304 """ 

3305 # start with an empty mesh 

3306 copied = Trimesh() 

3307 # always deepcopy vertex and face data 

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

3309 

3310 if include_visual: 

3311 # copy visual information 

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

3313 

3314 copied.vertex_attributes.update( 

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

3316 ) 

3317 copied.face_attributes.update( 

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

3319 ) 

3320 

3321 # get metadata 

3322 copied.metadata = deepcopy(self.metadata) 

3323 

3324 # make sure cache ID is set initially 

3325 copied._cache.verify() 

3326 

3327 if include_cache: 

3328 # shallow copy cached items into the new cache 

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

3330 # data in the new mesh is changed these items 

3331 # will be dumped in the new mesh but preserved 

3332 # in the original mesh 

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

3334 

3335 return copied 

3336 

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

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

3339 return self.copy(include_cache=False) 

3340 

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

3342 # interpret shallow copy as "keep cached data" 

3343 return self.copy(include_cache=True) 

3344 

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

3346 """ 

3347 DEPRECATED: call `eval` directly instead. 

3348 

3349 Evaluate a statement and cache the result before returning. 

3350 

3351 Parameters 

3352 ------------ 

3353 statement : str 

3354 Statement of valid python code 

3355 *args : list 

3356 Available inside statement as args[0], etc 

3357 

3358 Returns 

3359 ----------- 

3360 result : result of running eval on statement with args 

3361 

3362 Examples 

3363 ----------- 

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

3365 """ 

3366 import warnings 

3367 

3368 warnings.warn( 

3369 "`Trimesh.eval_cached` is deprecated " 

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

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

3372 category=DeprecationWarning, 

3373 stacklevel=2, 

3374 ) 

3375 

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

3377 hashable = [hash(statement)] 

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

3379 

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

3381 

3382 if key in self._cache: 

3383 return self._cache[key] 

3384 

3385 result = eval(statement) 

3386 self._cache[key] = result 

3387 return result 

3388 

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

3390 """ 

3391 Concatenate the mesh with another mesh. 

3392 

3393 Parameters 

3394 ------------ 

3395 other : trimesh.Trimesh object 

3396 Mesh to be concatenated with self 

3397 

3398 Returns 

3399 ---------- 

3400 concat : trimesh.Trimesh 

3401 Mesh object of combined result 

3402 """ 

3403 concat = util.concatenate(self, other) 

3404 return concat