vedo.mesh
Submodule to work with polygonal meshes
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3import numpy as np 4from typing import List, Tuple, Union, MutableSequence, Any 5from typing_extensions import Self 6 7import vedo.vtkclasses as vtki # a wrapper for lazy imports 8 9import vedo 10from vedo.colors import get_color 11from vedo.pointcloud import Points 12from vedo.utils import buildPolyData, is_sequence, mag, precision 13from vedo.utils import numpy2vtk, vtk2numpy, OperationNode 14from vedo.visual import MeshVisual 15 16__docformat__ = "google" 17 18__doc__ = """ 19Submodule to work with polygonal meshes 20 21![](https://vedo.embl.es/images/advanced/mesh_smoother2.png) 22""" 23 24__all__ = ["Mesh"] 25 26 27#################################################### 28class Mesh(MeshVisual, Points): 29 """ 30 Build an instance of object `Mesh` derived from `vedo.PointCloud`. 31 """ 32 33 def __init__(self, inputobj=None, c="gold", alpha=1): 34 """ 35 Initialize a ``Mesh`` object. 36 37 Arguments: 38 inputobj : (str, vtkPolyData, vtkActor, vedo.Mesh) 39 If inputobj is `None` an empty mesh is created. 40 If inputobj is a `str` then it is interpreted as the name of a file to load as mesh. 41 If inputobj is an `vtkPolyData` or `vtkActor` or `vedo.Mesh` 42 then a shallow copy of it is created. 43 If inputobj is a `vedo.Mesh` then a shallow copy of it is created. 44 45 Examples: 46 - [buildmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buildmesh.py) 47 (and many others!) 48 49 ![](https://vedo.embl.es/images/basic/buildmesh.png) 50 """ 51 # print("INIT MESH", super()) 52 super().__init__() 53 54 self.name = "Mesh" 55 56 if inputobj is None: 57 # self.dataset = vtki.vtkPolyData() 58 pass 59 60 elif isinstance(inputobj, str): 61 self.dataset = vedo.file_io.load(inputobj).dataset 62 self.filename = inputobj 63 64 elif isinstance(inputobj, vtki.vtkPolyData): 65 # self.dataset.DeepCopy(inputobj) # NO 66 self.dataset = inputobj 67 if self.dataset.GetNumberOfCells() == 0: 68 carr = vtki.vtkCellArray() 69 for i in range(inputobj.GetNumberOfPoints()): 70 carr.InsertNextCell(1) 71 carr.InsertCellPoint(i) 72 self.dataset.SetVerts(carr) 73 74 elif isinstance(inputobj, Mesh): 75 self.dataset = inputobj.dataset 76 77 elif is_sequence(inputobj): 78 ninp = len(inputobj) 79 if ninp == 4: # assume input is [vertices, faces, lines, strips] 80 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2], inputobj[3]) 81 elif ninp == 3: # assume input is [vertices, faces, lines] 82 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2]) 83 elif ninp == 2: # assume input is [vertices, faces] 84 self.dataset = buildPolyData(inputobj[0], inputobj[1]) 85 elif ninp == 1: # assume input is [vertices] 86 self.dataset = buildPolyData(inputobj[0]) 87 else: 88 vedo.logger.error("input must be a list of max 4 elements.") 89 raise ValueError() 90 91 elif isinstance(inputobj, vtki.vtkActor): 92 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 93 v = inputobj.GetMapper().GetScalarVisibility() 94 self.mapper.SetScalarVisibility(v) 95 pr = vtki.vtkProperty() 96 pr.DeepCopy(inputobj.GetProperty()) 97 self.actor.SetProperty(pr) 98 self.properties = pr 99 100 elif isinstance(inputobj, (vtki.vtkStructuredGrid, vtki.vtkRectilinearGrid)): 101 gf = vtki.new("GeometryFilter") 102 gf.SetInputData(inputobj) 103 gf.Update() 104 self.dataset = gf.GetOutput() 105 106 elif "meshlab" in str(type(inputobj)): 107 self.dataset = vedo.utils.meshlab2vedo(inputobj).dataset 108 109 elif "meshlib" in str(type(inputobj)): 110 import meshlib.mrmeshnumpy as mrmeshnumpy 111 self.dataset = buildPolyData( 112 mrmeshnumpy.getNumpyVerts(inputobj), 113 mrmeshnumpy.getNumpyFaces(inputobj.topology), 114 ) 115 116 elif "trimesh" in str(type(inputobj)): 117 self.dataset = vedo.utils.trimesh2vedo(inputobj).dataset 118 119 elif "meshio" in str(type(inputobj)): 120 # self.dataset = vedo.utils.meshio2vedo(inputobj) ##TODO 121 if len(inputobj.cells) > 0: 122 mcells = [] 123 for cellblock in inputobj.cells: 124 if cellblock.type in ("triangle", "quad"): 125 mcells += cellblock.data.tolist() 126 self.dataset = buildPolyData(inputobj.points, mcells) 127 else: 128 self.dataset = buildPolyData(inputobj.points, None) 129 # add arrays: 130 try: 131 if len(inputobj.point_data) > 0: 132 for k in inputobj.point_data.keys(): 133 vdata = numpy2vtk(inputobj.point_data[k]) 134 vdata.SetName(str(k)) 135 self.dataset.GetPointData().AddArray(vdata) 136 except AssertionError: 137 print("Could not add meshio point data, skip.") 138 139 else: 140 try: 141 gf = vtki.new("GeometryFilter") 142 gf.SetInputData(inputobj) 143 gf.Update() 144 self.dataset = gf.GetOutput() 145 except: 146 vedo.logger.error(f"cannot build mesh from type {type(inputobj)}") 147 raise RuntimeError() 148 149 self.mapper.SetInputData(self.dataset) 150 self.actor.SetMapper(self.mapper) 151 152 self.properties.SetInterpolationToPhong() 153 self.properties.SetColor(get_color(c)) 154 155 if alpha is not None: 156 self.properties.SetOpacity(alpha) 157 158 self.mapper.SetInterpolateScalarsBeforeMapping( 159 vedo.settings.interpolate_scalars_before_mapping 160 ) 161 162 if vedo.settings.use_polygon_offset: 163 self.mapper.SetResolveCoincidentTopologyToPolygonOffset() 164 pof = vedo.settings.polygon_offset_factor 165 pou = vedo.settings.polygon_offset_units 166 self.mapper.SetResolveCoincidentTopologyPolygonOffsetParameters(pof, pou) 167 168 n = self.dataset.GetNumberOfPoints() 169 self.pipeline = OperationNode(self, comment=f"#pts {n}") 170 171 def _repr_html_(self): 172 """ 173 HTML representation of the Mesh object for Jupyter Notebooks. 174 175 Returns: 176 HTML text with the image and some properties. 177 """ 178 import io 179 import base64 180 from PIL import Image 181 182 library_name = "vedo.mesh.Mesh" 183 help_url = "https://vedo.embl.es/docs/vedo/mesh.html#Mesh" 184 185 arr = self.thumbnail() 186 im = Image.fromarray(arr) 187 buffered = io.BytesIO() 188 im.save(buffered, format="PNG", quality=100) 189 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 190 url = "data:image/png;base64," + encoded 191 image = f"<img src='{url}'></img>" 192 193 bounds = "<br/>".join( 194 [ 195 precision(min_x, 4) + " ... " + precision(max_x, 4) 196 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 197 ] 198 ) 199 average_size = "{size:.3f}".format(size=self.average_size()) 200 201 help_text = "" 202 if self.name: 203 help_text += f"<b> {self.name}:   </b>" 204 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 205 if self.filename: 206 dots = "" 207 if len(self.filename) > 30: 208 dots = "..." 209 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 210 211 pdata = "" 212 if self.dataset.GetPointData().GetScalars(): 213 if self.dataset.GetPointData().GetScalars().GetName(): 214 name = self.dataset.GetPointData().GetScalars().GetName() 215 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 216 217 cdata = "" 218 if self.dataset.GetCellData().GetScalars(): 219 if self.dataset.GetCellData().GetScalars().GetName(): 220 name = self.dataset.GetCellData().GetScalars().GetName() 221 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 222 223 allt = [ 224 "<table>", 225 "<tr>", 226 "<td>", 227 image, 228 "</td>", 229 "<td style='text-align: center; vertical-align: center;'><br/>", 230 help_text, 231 "<table>", 232 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 233 "<tr><td><b> center of mass </b></td><td>" 234 + precision(self.center_of_mass(), 3) 235 + "</td></tr>", 236 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 237 "<tr><td><b> nr. points / faces </b></td><td>" 238 + str(self.npoints) 239 + " / " 240 + str(self.ncells) 241 + "</td></tr>", 242 pdata, 243 cdata, 244 "</table>", 245 "</table>", 246 ] 247 return "\n".join(allt) 248 249 def faces(self, ids=()): 250 """DEPRECATED. Use property `mesh.cells` instead.""" 251 vedo.printc("WARNING: use property mesh.cells instead of mesh.faces()",c='y') 252 return self.cells 253 254 @property 255 def edges(self): 256 """Return an array containing the edges connectivity.""" 257 extractEdges = vtki.new("ExtractEdges") 258 extractEdges.SetInputData(self.dataset) 259 # eed.UseAllPointsOn() 260 extractEdges.Update() 261 lpoly = extractEdges.GetOutput() 262 263 arr1d = vtk2numpy(lpoly.GetLines().GetData()) 264 # [nids1, id0 ... idn, niids2, id0 ... idm, etc]. 265 266 i = 0 267 conn = [] 268 n = len(arr1d) 269 for _ in range(n): 270 cell = [arr1d[i + k + 1] for k in range(arr1d[i])] 271 conn.append(cell) 272 i += arr1d[i] + 1 273 if i >= n: 274 break 275 return conn # cannot always make a numpy array of it! 276 277 @property 278 def cell_normals(self): 279 """ 280 Retrieve face normals as a numpy array. 281 Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`. 282 """ 283 vtknormals = self.dataset.GetCellData().GetNormals() 284 numpy_normals = vtk2numpy(vtknormals) 285 if len(numpy_normals) == 0 and len(self.cells) != 0: 286 vedo.logger.warning( 287 "failed to return normal vectors.\n" 288 "You may need to call `Mesh.compute_normals()` before accessing 'Mesh.cell_normals'." 289 ) 290 numpy_normals = np.zeros((self.ncells, 3)) + [0,0,1] 291 return numpy_normals 292 293 def compute_normals(self, points=True, cells=True, feature_angle=None, consistency=True) -> Self: 294 """ 295 Compute cell and vertex normals for the mesh. 296 297 Arguments: 298 points : (bool) 299 do the computation for the vertices too 300 cells : (bool) 301 do the computation for the cells too 302 feature_angle : (float) 303 specify the angle that defines a sharp edge. 304 If the difference in angle across neighboring polygons is greater than this value, 305 the shared edge is considered "sharp" and it is split. 306 consistency : (bool) 307 turn on/off the enforcement of consistent polygon ordering. 308 309 .. warning:: 310 If `feature_angle` is set then the Mesh can be modified, and it 311 can have a different nr. of vertices from the original. 312 313 Note that the appearance of the mesh may change if the normals are computed, 314 as shading is automatically enabled when such information is present. 315 Use `mesh.flat()` to avoid smoothing effects. 316 """ 317 pdnorm = vtki.new("PolyDataNormals") 318 pdnorm.SetInputData(self.dataset) 319 pdnorm.SetComputePointNormals(points) 320 pdnorm.SetComputeCellNormals(cells) 321 pdnorm.SetConsistency(consistency) 322 pdnorm.FlipNormalsOff() 323 if feature_angle: 324 pdnorm.SetSplitting(True) 325 pdnorm.SetFeatureAngle(feature_angle) 326 else: 327 pdnorm.SetSplitting(False) 328 pdnorm.Update() 329 out = pdnorm.GetOutput() 330 self._update(out, reset_locators=False) 331 return self 332 333 def reverse(self, cells=True, normals=False) -> Self: 334 """ 335 Reverse the order of polygonal cells 336 and/or reverse the direction of point and cell normals. 337 338 Two flags are used to control these operations: 339 - `cells=True` reverses the order of the indices in the cell connectivity list. 340 If cell is a list of IDs only those cells will be reversed. 341 - `normals=True` reverses the normals by multiplying the normal vector by -1 342 (both point and cell normals, if present). 343 """ 344 poly = self.dataset 345 346 if is_sequence(cells): 347 for cell in cells: 348 poly.ReverseCell(cell) 349 poly.GetCellData().Modified() 350 return self ############## 351 352 rev = vtki.new("ReverseSense") 353 if cells: 354 rev.ReverseCellsOn() 355 else: 356 rev.ReverseCellsOff() 357 if normals: 358 rev.ReverseNormalsOn() 359 else: 360 rev.ReverseNormalsOff() 361 rev.SetInputData(poly) 362 rev.Update() 363 self._update(rev.GetOutput(), reset_locators=False) 364 self.pipeline = OperationNode("reverse", parents=[self]) 365 return self 366 367 def volume(self) -> float: 368 """ 369 Compute the volume occupied by mesh. 370 The mesh must be triangular for this to work. 371 To triangulate a mesh use `mesh.triangulate()`. 372 """ 373 mass = vtki.new("MassProperties") 374 mass.SetGlobalWarningDisplay(0) 375 mass.SetInputData(self.dataset) 376 mass.Update() 377 mass.SetGlobalWarningDisplay(1) 378 return mass.GetVolume() 379 380 def area(self) -> float: 381 """ 382 Compute the surface area of the mesh. 383 The mesh must be triangular for this to work. 384 To triangulate a mesh use `mesh.triangulate()`. 385 """ 386 mass = vtki.new("MassProperties") 387 mass.SetGlobalWarningDisplay(0) 388 mass.SetInputData(self.dataset) 389 mass.Update() 390 mass.SetGlobalWarningDisplay(1) 391 return mass.GetSurfaceArea() 392 393 def is_closed(self) -> bool: 394 """ 395 Return `True` if the mesh is watertight. 396 Note that if the mesh contains coincident points the result may be flase. 397 Use in this case `mesh.clean()` to merge coincident points. 398 """ 399 fe = vtki.new("FeatureEdges") 400 fe.BoundaryEdgesOn() 401 fe.FeatureEdgesOff() 402 fe.NonManifoldEdgesOn() 403 fe.SetInputData(self.dataset) 404 fe.Update() 405 ne = fe.GetOutput().GetNumberOfCells() 406 return not bool(ne) 407 408 def is_manifold(self) -> bool: 409 """Return `True` if the mesh is manifold.""" 410 fe = vtki.new("FeatureEdges") 411 fe.BoundaryEdgesOff() 412 fe.FeatureEdgesOff() 413 fe.NonManifoldEdgesOn() 414 fe.SetInputData(self.dataset) 415 fe.Update() 416 ne = fe.GetOutput().GetNumberOfCells() 417 return not bool(ne) 418 419 def non_manifold_faces(self, remove=True, tol="auto") -> Self: 420 """ 421 Detect and (try to) remove non-manifold faces of a triangular mesh: 422 423 - set `remove` to `False` to mark cells without removing them. 424 - set `tol=0` for zero-tolerance, the result will be manifold but with holes. 425 - set `tol>0` to cut off non-manifold faces, and try to recover the good ones. 426 - set `tol="auto"` to make an automatic choice of the tolerance. 427 """ 428 # mark original point and cell ids 429 self.add_ids() 430 toremove = self.boundaries( 431 boundary_edges=False, 432 non_manifold_edges=True, 433 cell_edge=True, 434 return_cell_ids=True, 435 ) 436 if len(toremove) == 0: # type: ignore 437 return self 438 439 points = self.vertices 440 faces = self.cells 441 centers = self.cell_centers 442 443 copy = self.clone() 444 copy.delete_cells(toremove).clean() 445 copy.compute_normals(cells=False) 446 normals = copy.vertex_normals 447 deltas, deltas_i = [], [] 448 449 for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"): 450 pids = copy.closest_point(centers[i], n=3, return_point_id=True) 451 norms = normals[pids] 452 n = np.mean(norms, axis=0) 453 dn = np.linalg.norm(n) 454 if not dn: 455 continue 456 n = n / dn 457 458 p0, p1, p2 = points[faces[i]][:3] 459 v = np.cross(p1 - p0, p2 - p0) 460 lv = np.linalg.norm(v) 461 if not lv: 462 continue 463 v = v / lv 464 465 cosa = 1 - np.dot(n, v) 466 deltas.append(cosa) 467 deltas_i.append(i) 468 469 recover = [] 470 if len(deltas) > 0: 471 mean_delta = np.mean(deltas) 472 err_delta = np.std(deltas) 473 txt = "" 474 if tol == "auto": # automatic choice 475 tol = mean_delta / 5 476 txt = f"\n Automatic tol. : {tol: .4f}" 477 for i, cosa in zip(deltas_i, deltas): 478 if cosa < tol: 479 recover.append(i) 480 481 vedo.logger.info( 482 f"\n --------- Non manifold faces ---------" 483 f"\n Average tol. : {mean_delta: .4f} +- {err_delta: .4f}{txt}" 484 f"\n Removed faces : {len(toremove)}" # type: ignore 485 f"\n Recovered faces: {len(recover)}" 486 ) 487 488 toremove = list(set(toremove) - set(recover)) # type: ignore 489 490 if not remove: 491 mark = np.zeros(self.ncells, dtype=np.uint8) 492 mark[recover] = 1 493 mark[toremove] = 2 494 self.celldata["NonManifoldCell"] = mark 495 else: 496 self.delete_cells(toremove) # type: ignore 497 498 self.pipeline = OperationNode( 499 "non_manifold_faces", 500 parents=[self], 501 comment=f"#cells {self.dataset.GetNumberOfCells()}", 502 ) 503 return self 504 505 506 def euler_characteristic(self) -> int: 507 """ 508 Compute the Euler characteristic of the mesh. 509 The Euler characteristic is a topological invariant for surfaces. 510 """ 511 return self.npoints - len(self.edges) + self.ncells 512 513 def genus(self) -> int: 514 """ 515 Compute the genus of the mesh. 516 The genus is a topological invariant for surfaces. 517 """ 518 nb = len(self.boundaries().split()) - 1 519 return (2 - self.euler_characteristic() - nb ) / 2 520 521 def to_reeb_graph(self, field_id=0): 522 """ 523 Convert the mesh into a Reeb graph. 524 The Reeb graph is a topological structure that captures the evolution 525 of the level sets of a scalar field. 526 527 Arguments: 528 field_id : (int) 529 the id of the scalar field to use. 530 531 Example: 532 ```python 533 from vedo import * 534 mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl") 535 mesh.rotate_x(10).rotate_y(15).alpha(0.5) 536 mesh.pointdata["scalars"] = mesh.vertices[:, 2] 537 538 printc("is_closed :", mesh.is_closed()) 539 printc("is_manifold:", mesh.is_manifold()) 540 printc("euler_char :", mesh.euler_characteristic()) 541 printc("genus :", mesh.genus()) 542 543 reeb = mesh.to_reeb_graph() 544 ids = reeb[0].pointdata["Vertex Ids"] 545 pts = Points(mesh.vertices[ids], r=10) 546 547 show([[mesh, pts], reeb], N=2, sharecam=False) 548 ``` 549 """ 550 rg = vtki.new("PolyDataToReebGraphFilter") 551 rg.SetInputData(self.dataset) 552 rg.SetFieldId(field_id) 553 rg.Update() 554 gr = vedo.pyplot.DirectedGraph() 555 gr.mdg = rg.GetOutput() 556 gr.build() 557 return gr 558 559 560 def shrink(self, fraction=0.85) -> Self: 561 """ 562 Shrink the triangle polydata in the representation of the input mesh. 563 564 Examples: 565 - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py) 566 567 ![](https://vedo.embl.es/images/basic/shrink.png) 568 """ 569 # Overriding base class method core.shrink() 570 shrink = vtki.new("ShrinkPolyData") 571 shrink.SetInputData(self.dataset) 572 shrink.SetShrinkFactor(fraction) 573 shrink.Update() 574 self._update(shrink.GetOutput()) 575 self.pipeline = OperationNode("shrink", parents=[self]) 576 return self 577 578 def cap(self, return_cap=False) -> Self: 579 """ 580 Generate a "cap" on a clipped mesh, or caps sharp edges. 581 582 Examples: 583 - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py) 584 585 ![](https://vedo.embl.es/images/advanced/cutAndCap.png) 586 587 See also: `join()`, `join_segments()`, `slice()`. 588 """ 589 fe = vtki.new("FeatureEdges") 590 fe.SetInputData(self.dataset) 591 fe.BoundaryEdgesOn() 592 fe.FeatureEdgesOff() 593 fe.NonManifoldEdgesOff() 594 fe.ManifoldEdgesOff() 595 fe.Update() 596 597 stripper = vtki.new("Stripper") 598 stripper.SetInputData(fe.GetOutput()) 599 stripper.JoinContiguousSegmentsOn() 600 stripper.Update() 601 602 boundary_poly = vtki.vtkPolyData() 603 boundary_poly.SetPoints(stripper.GetOutput().GetPoints()) 604 boundary_poly.SetPolys(stripper.GetOutput().GetLines()) 605 606 rev = vtki.new("ReverseSense") 607 rev.ReverseCellsOn() 608 rev.SetInputData(boundary_poly) 609 rev.Update() 610 611 tf = vtki.new("TriangleFilter") 612 tf.SetInputData(rev.GetOutput()) 613 tf.Update() 614 615 if return_cap: 616 m = Mesh(tf.GetOutput()) 617 m.pipeline = OperationNode( 618 "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}" 619 ) 620 m.name = "MeshCap" 621 return m 622 623 polyapp = vtki.new("AppendPolyData") 624 polyapp.AddInputData(self.dataset) 625 polyapp.AddInputData(tf.GetOutput()) 626 polyapp.Update() 627 628 self._update(polyapp.GetOutput()) 629 self.clean() 630 631 self.pipeline = OperationNode( 632 "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 633 ) 634 return self 635 636 def join(self, polys=True, reset=False) -> Self: 637 """ 638 Generate triangle strips and/or polylines from 639 input polygons, triangle strips, and lines. 640 641 Input polygons are assembled into triangle strips only if they are triangles; 642 other types of polygons are passed through to the output and not stripped. 643 Use mesh.triangulate() to triangulate non-triangular polygons prior to running 644 this filter if you need to strip all the data. 645 646 Also note that if triangle strips or polylines are present in the input 647 they are passed through and not joined nor extended. 648 If you wish to strip these use mesh.triangulate() to fragment the input 649 into triangles and lines prior to applying join(). 650 651 Arguments: 652 polys : (bool) 653 polygonal segments will be joined if they are contiguous 654 reset : (bool) 655 reset points ordering 656 657 Warning: 658 If triangle strips or polylines exist in the input data 659 they will be passed through to the output data. 660 This filter will only construct triangle strips if triangle polygons 661 are available; and will only construct polylines if lines are available. 662 663 Example: 664 ```python 665 from vedo import * 666 c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate() 667 c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate() 668 intersect = c1.intersect_with(c2).join(reset=True) 669 spline = Spline(intersect).c('blue').lw(5) 670 show(c1, c2, spline, intersect.labels('id'), axes=1).close() 671 ``` 672 ![](https://vedo.embl.es/images/feats/line_join.png) 673 """ 674 sf = vtki.new("Stripper") 675 sf.SetPassThroughCellIds(True) 676 sf.SetPassThroughPointIds(True) 677 sf.SetJoinContiguousSegments(polys) 678 sf.SetInputData(self.dataset) 679 sf.Update() 680 if reset: 681 poly = sf.GetOutput() 682 cpd = vtki.new("CleanPolyData") 683 cpd.PointMergingOn() 684 cpd.ConvertLinesToPointsOn() 685 cpd.ConvertPolysToLinesOn() 686 cpd.ConvertStripsToPolysOn() 687 cpd.SetInputData(poly) 688 cpd.Update() 689 poly = cpd.GetOutput() 690 vpts = poly.GetCell(0).GetPoints().GetData() 691 poly.GetPoints().SetData(vpts) 692 else: 693 poly = sf.GetOutput() 694 695 self._update(poly) 696 697 self.pipeline = OperationNode( 698 "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 699 ) 700 return self 701 702 def join_segments(self, closed=True, tol=1e-03) -> list: 703 """ 704 Join line segments into contiguous lines. 705 Useful to call with `triangulate()` method. 706 707 Returns: 708 list of `shapes.Lines` 709 710 Example: 711 ```python 712 from vedo import * 713 msh = Torus().alpha(0.1).wireframe() 714 intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5') 715 slices = [s.triangulate() for s in intersection.join_segments()] 716 show(msh, intersection, merge(slices), axes=1, viewup='z') 717 ``` 718 ![](https://vedo.embl.es/images/feats/join_segments.jpg) 719 """ 720 vlines = [] 721 for ipiece, outline in enumerate(self.split(must_share_edge=False)): # type: ignore 722 723 outline.clean() 724 pts = outline.vertices 725 if len(pts) < 3: 726 continue 727 avesize = outline.average_size() 728 lines = outline.lines 729 # print("---lines", lines, "in piece", ipiece) 730 tol = avesize / pts.shape[0] * tol 731 732 k = 0 733 joinedpts = [pts[k]] 734 for _ in range(len(pts)): 735 pk = pts[k] 736 for j, line in enumerate(lines): 737 738 id0, id1 = line[0], line[-1] 739 p0, p1 = pts[id0], pts[id1] 740 741 if np.linalg.norm(p0 - pk) < tol: 742 n = len(line) 743 for m in range(1, n): 744 joinedpts.append(pts[line[m]]) 745 # joinedpts.append(p1) 746 k = id1 747 lines.pop(j) 748 break 749 750 elif np.linalg.norm(p1 - pk) < tol: 751 n = len(line) 752 for m in reversed(range(0, n - 1)): 753 joinedpts.append(pts[line[m]]) 754 # joinedpts.append(p0) 755 k = id0 756 lines.pop(j) 757 break 758 759 if len(joinedpts) > 1: 760 newline = vedo.shapes.Line(joinedpts, closed=closed) 761 newline.clean() 762 newline.actor.SetProperty(self.properties) 763 newline.properties = self.properties 764 newline.pipeline = OperationNode( 765 "join_segments", 766 parents=[self], 767 comment=f"#pts {newline.dataset.GetNumberOfPoints()}", 768 ) 769 vlines.append(newline) 770 771 return vlines 772 773 def join_with_strips(self, b1, closed=True) -> Self: 774 """ 775 Join booundary lines by creating a triangle strip between them. 776 777 Example: 778 ```python 779 from vedo import * 780 m1 = Cylinder(cap=False).boundaries() 781 m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1) 782 strips = m1.join_with_strips(m2) 783 show(m1, m2, strips, axes=1).close() 784 ``` 785 """ 786 b0 = self.clone().join() 787 b1 = b1.clone().join() 788 789 vertices0 = b0.vertices.tolist() 790 vertices1 = b1.vertices.tolist() 791 792 lines0 = b0.lines 793 lines1 = b1.lines 794 m = len(lines0) 795 assert m == len(lines1), ( 796 "lines must have the same number of points\n" 797 f"line has {m} points in b0 and {len(lines1)} in b1" 798 ) 799 800 strips = [] 801 points: List[Any] = [] 802 803 for j in range(m): 804 805 ids0j = list(lines0[j]) 806 ids1j = list(lines1[j]) 807 808 n = len(ids0j) 809 assert n == len(ids1j), ( 810 "lines must have the same number of points\n" 811 f"line {j} has {n} points in b0 and {len(ids1j)} in b1" 812 ) 813 814 if closed: 815 ids0j.append(ids0j[0]) 816 ids1j.append(ids1j[0]) 817 vertices0.append(vertices0[ids0j[0]]) 818 vertices1.append(vertices1[ids1j[0]]) 819 n = n + 1 820 821 strip = [] # create a triangle strip 822 npt = len(points) 823 for ipt in range(n): 824 points.append(vertices0[ids0j[ipt]]) 825 points.append(vertices1[ids1j[ipt]]) 826 827 strip = list(range(npt, npt + 2*n)) 828 strips.append(strip) 829 830 return Mesh([points, [], [], strips], c="k6") 831 832 def split_polylines(self) -> Self: 833 """Split polylines into separate segments.""" 834 tf = vtki.new("TriangleFilter") 835 tf.SetPassLines(True) 836 tf.SetPassVerts(False) 837 tf.SetInputData(self.dataset) 838 tf.Update() 839 self._update(tf.GetOutput(), reset_locators=False) 840 self.lw(0).lighting("default").pickable() 841 self.pipeline = OperationNode( 842 "split_polylines", parents=[self], 843 comment=f"#lines {self.dataset.GetNumberOfLines()}" 844 ) 845 return self 846 847 def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 848 """ 849 Slice a mesh with a plane and fill the contour. 850 851 Example: 852 ```python 853 from vedo import * 854 msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe() 855 mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0]) 856 mslice.c('purple5') 857 show(msh, mslice, axes=1) 858 ``` 859 ![](https://vedo.embl.es/images/feats/mesh_slice.jpg) 860 861 See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`. 862 """ 863 intersection = self.intersect_with_plane(origin=origin, normal=normal) 864 slices = [s.triangulate() for s in intersection.join_segments()] 865 mslices = vedo.pointcloud.merge(slices) 866 if mslices: 867 mslices.name = "MeshSlice" 868 mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}") 869 return mslices 870 871 def triangulate(self, verts=True, lines=True) -> Self: 872 """ 873 Converts mesh polygons into triangles. 874 875 If the input mesh is only made of 2D lines (no faces) the output will be a triangulation 876 that fills the internal area. The contours may be concave, and may even contain holes, 877 i.e. a contour may contain an internal contour winding in the opposite 878 direction to indicate that it is a hole. 879 880 Arguments: 881 verts : (bool) 882 if True, break input vertex cells into individual vertex cells (one point per cell). 883 If False, the input vertex cells will be ignored. 884 lines : (bool) 885 if True, break input polylines into line segments. 886 If False, input lines will be ignored and the output will have no lines. 887 """ 888 if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips(): 889 # print("Using vtkTriangleFilter") 890 tf = vtki.new("TriangleFilter") 891 tf.SetPassLines(lines) 892 tf.SetPassVerts(verts) 893 894 elif self.dataset.GetNumberOfLines(): 895 # print("Using vtkContourTriangulator") 896 tf = vtki.new("ContourTriangulator") 897 tf.TriangulationErrorDisplayOn() 898 899 else: 900 vedo.logger.debug("input in triangulate() seems to be void! Skip.") 901 return self 902 903 tf.SetInputData(self.dataset) 904 tf.Update() 905 self._update(tf.GetOutput(), reset_locators=False) 906 self.lw(0).lighting("default").pickable() 907 908 self.pipeline = OperationNode( 909 "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}" 910 ) 911 return self 912 913 def compute_cell_vertex_count(self) -> Self: 914 """ 915 Add to this mesh a cell data array containing the nr of vertices that a polygonal face has. 916 """ 917 csf = vtki.new("CellSizeFilter") 918 csf.SetInputData(self.dataset) 919 csf.SetComputeArea(False) 920 csf.SetComputeVolume(False) 921 csf.SetComputeLength(False) 922 csf.SetComputeVertexCount(True) 923 csf.SetVertexCountArrayName("VertexCount") 924 csf.Update() 925 self.dataset.GetCellData().AddArray( 926 csf.GetOutput().GetCellData().GetArray("VertexCount") 927 ) 928 return self 929 930 def compute_quality(self, metric=6) -> Self: 931 """ 932 Calculate metrics of quality for the elements of a triangular mesh. 933 This method adds to the mesh a cell array named "Quality". 934 See class 935 [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html). 936 937 Arguments: 938 metric : (int) 939 type of available estimators are: 940 - EDGE RATIO, 0 941 - ASPECT RATIO, 1 942 - RADIUS RATIO, 2 943 - ASPECT FROBENIUS, 3 944 - MED ASPECT FROBENIUS, 4 945 - MAX ASPECT FROBENIUS, 5 946 - MIN_ANGLE, 6 947 - COLLAPSE RATIO, 7 948 - MAX ANGLE, 8 949 - CONDITION, 9 950 - SCALED JACOBIAN, 10 951 - SHEAR, 11 952 - RELATIVE SIZE SQUARED, 12 953 - SHAPE, 13 954 - SHAPE AND SIZE, 14 955 - DISTORTION, 15 956 - MAX EDGE RATIO, 16 957 - SKEW, 17 958 - TAPER, 18 959 - VOLUME, 19 960 - STRETCH, 20 961 - DIAGONAL, 21 962 - DIMENSION, 22 963 - ODDY, 23 964 - SHEAR AND SIZE, 24 965 - JACOBIAN, 25 966 - WARPAGE, 26 967 - ASPECT GAMMA, 27 968 - AREA, 28 969 - ASPECT BETA, 29 970 971 Examples: 972 - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py) 973 974 ![](https://vedo.embl.es/images/advanced/meshquality.png) 975 """ 976 qf = vtki.new("MeshQuality") 977 qf.SetInputData(self.dataset) 978 qf.SetTriangleQualityMeasure(metric) 979 qf.SaveCellQualityOn() 980 qf.Update() 981 self._update(qf.GetOutput(), reset_locators=False) 982 self.mapper.SetScalarModeToUseCellData() 983 self.pipeline = OperationNode("compute_quality", parents=[self]) 984 return self 985 986 def count_vertices(self) -> np.ndarray: 987 """Count the number of vertices each cell has and return it as a numpy array""" 988 vc = vtki.new("CountVertices") 989 vc.SetInputData(self.dataset) 990 vc.SetOutputArrayName("VertexCount") 991 vc.Update() 992 varr = vc.GetOutput().GetCellData().GetArray("VertexCount") 993 return vtk2numpy(varr) 994 995 def check_validity(self, tol=0) -> np.ndarray: 996 """ 997 Return a numpy array of possible problematic faces following this convention: 998 - Valid = 0 999 - WrongNumberOfPoints = 1 1000 - IntersectingEdges = 2 1001 - IntersectingFaces = 4 1002 - NoncontiguousEdges = 8 1003 - Nonconvex = 10 1004 - OrientedIncorrectly = 20 1005 1006 Arguments: 1007 tol : (float) 1008 value is used as an epsilon for floating point 1009 equality checks throughout the cell checking process. 1010 """ 1011 vald = vtki.new("CellValidator") 1012 if tol: 1013 vald.SetTolerance(tol) 1014 vald.SetInputData(self.dataset) 1015 vald.Update() 1016 varr = vald.GetOutput().GetCellData().GetArray("ValidityState") 1017 return vtk2numpy(varr) 1018 1019 def compute_curvature(self, method=0) -> Self: 1020 """ 1021 Add scalars to `Mesh` that contains the curvature calculated in three different ways. 1022 1023 Variable `method` can be: 1024 - 0 = gaussian 1025 - 1 = mean curvature 1026 - 2 = max curvature 1027 - 3 = min curvature 1028 1029 Example: 1030 ```python 1031 from vedo import Torus 1032 Torus().compute_curvature().add_scalarbar().show().close() 1033 ``` 1034 ![](https://vedo.embl.es/images/advanced/torus_curv.png) 1035 """ 1036 curve = vtki.new("Curvatures") 1037 curve.SetInputData(self.dataset) 1038 curve.SetCurvatureType(method) 1039 curve.Update() 1040 self._update(curve.GetOutput(), reset_locators=False) 1041 self.mapper.ScalarVisibilityOn() 1042 return self 1043 1044 def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self: 1045 """ 1046 Add to `Mesh` a scalar array that contains distance along a specified direction. 1047 1048 Arguments: 1049 low : (list) 1050 one end of the line (small scalar values) 1051 high : (list) 1052 other end of the line (large scalar values) 1053 vrange : (list) 1054 set the range of the scalar 1055 1056 Example: 1057 ```python 1058 from vedo import Sphere 1059 s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1)) 1060 s.add_scalarbar().show(axes=1).close() 1061 ``` 1062 ![](https://vedo.embl.es/images/basic/compute_elevation.png) 1063 """ 1064 ef = vtki.new("ElevationFilter") 1065 ef.SetInputData(self.dataset) 1066 ef.SetLowPoint(low) 1067 ef.SetHighPoint(high) 1068 ef.SetScalarRange(vrange) 1069 ef.Update() 1070 self._update(ef.GetOutput(), reset_locators=False) 1071 self.mapper.ScalarVisibilityOn() 1072 return self 1073 1074 1075 def laplacian_diffusion(self, array_name, dt, num_steps) -> Self: 1076 """ 1077 Apply a diffusion process to a scalar array defined on the points of a mesh. 1078 1079 Arguments: 1080 array_name : (str) 1081 name of the array to diffuse. 1082 dt : (float) 1083 time step. 1084 num_steps : (int) 1085 number of iterations. 1086 """ 1087 try: 1088 import scipy.sparse 1089 import scipy.sparse.linalg 1090 except ImportError: 1091 vedo.logger.error("scipy not found. Cannot run laplacian_diffusion()") 1092 return self 1093 1094 def build_laplacian(): 1095 rows = [] 1096 cols = [] 1097 data = [] 1098 n_points = points.shape[0] 1099 avg_area = np.mean(areas) * 10000 1100 # print("avg_area", avg_area) 1101 1102 for triangle in cells: 1103 for i in range(3): 1104 for j in range(i + 1, 3): 1105 u = triangle[i] 1106 v = triangle[j] 1107 rows.append(u) 1108 cols.append(v) 1109 rows.append(v) 1110 cols.append(u) 1111 data.append(-1/avg_area) 1112 data.append(-1/avg_area) 1113 1114 L = scipy.sparse.coo_matrix( 1115 (data, (rows, cols)), shape=(n_points, n_points) 1116 ).tocsc() 1117 1118 degree = -np.array(L.sum(axis=1)).flatten() # adjust the diagonal 1119 # print("degree", degree) 1120 L.setdiag(degree) 1121 return L 1122 1123 def _diffuse(u0, L, dt, num_steps): 1124 # mean_area = np.mean(areas) * 10000 1125 # print("mean_area", mean_area) 1126 mean_area = 1 1127 I = scipy.sparse.eye(L.shape[0], format="csc") 1128 A = I - (dt/mean_area) * L 1129 u = u0 1130 for _ in range(int(num_steps)): 1131 u = A.dot(u) 1132 return u 1133 1134 self.compute_cell_size() 1135 areas = self.celldata["Area"] 1136 points = self.vertices 1137 cells = self.cells 1138 u0 = self.pointdata[array_name] 1139 1140 # Simulate diffusion 1141 L = build_laplacian() 1142 u = _diffuse(u0, L, dt, num_steps) 1143 self.pointdata[array_name] = u 1144 return self 1145 1146 1147 def subdivide(self, n=1, method=0, mel=None) -> Self: 1148 """ 1149 Increase the number of vertices of a surface mesh. 1150 1151 Arguments: 1152 n : (int) 1153 number of subdivisions. 1154 method : (int) 1155 Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4) 1156 mel : (float) 1157 Maximum Edge Length (applicable to Adaptive method only). 1158 """ 1159 triangles = vtki.new("TriangleFilter") 1160 triangles.SetInputData(self.dataset) 1161 triangles.Update() 1162 tri_mesh = triangles.GetOutput() 1163 if method == 0: 1164 sdf = vtki.new("LoopSubdivisionFilter") 1165 elif method == 1: 1166 sdf = vtki.new("LinearSubdivisionFilter") 1167 elif method == 2: 1168 sdf = vtki.new("AdaptiveSubdivisionFilter") 1169 if mel is None: 1170 mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n 1171 sdf.SetMaximumEdgeLength(mel) 1172 elif method == 3: 1173 sdf = vtki.new("ButterflySubdivisionFilter") 1174 elif method == 4: 1175 sdf = vtki.new("DensifyPolyData") 1176 else: 1177 vedo.logger.error(f"in subdivide() unknown method {method}") 1178 raise RuntimeError() 1179 1180 if method != 2: 1181 sdf.SetNumberOfSubdivisions(n) 1182 1183 sdf.SetInputData(tri_mesh) 1184 sdf.Update() 1185 1186 self._update(sdf.GetOutput()) 1187 1188 self.pipeline = OperationNode( 1189 "subdivide", 1190 parents=[self], 1191 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1192 ) 1193 return self 1194 1195 1196 def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self: 1197 """ 1198 Downsample the number of vertices in a mesh to `fraction`. 1199 1200 This filter preserves the `pointdata` of the input dataset. In previous versions 1201 of vedo, this decimation algorithm was referred to as quadric decimation. 1202 1203 Arguments: 1204 fraction : (float) 1205 the desired target of reduction. 1206 n : (int) 1207 the desired number of final points 1208 (`fraction` is recalculated based on it). 1209 preserve_volume : (bool) 1210 Decide whether to activate volume preservation which greatly 1211 reduces errors in triangle normal direction. 1212 regularization : (float) 1213 regularize the point finding algorithm so as to have better quality 1214 mesh elements at the cost of a slightly lower precision on the 1215 geometry potentially (mostly at sharp edges). 1216 Can be useful for decimating meshes that have been triangulated on noisy data. 1217 1218 Note: 1219 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1220 Internally the VTK class 1221 [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html) 1222 is used for this operation. 1223 1224 See also: `decimate_binned()` and `decimate_pro()`. 1225 """ 1226 poly = self.dataset 1227 if n: # N = desired number of points 1228 npt = poly.GetNumberOfPoints() 1229 fraction = n / npt 1230 if fraction >= 1: 1231 return self 1232 1233 decimate = vtki.new("QuadricDecimation") 1234 decimate.SetVolumePreservation(preserve_volume) 1235 # decimate.AttributeErrorMetricOn() 1236 if regularization: 1237 decimate.SetRegularize(True) 1238 decimate.SetRegularization(regularization) 1239 1240 try: 1241 decimate.MapPointDataOn() 1242 except AttributeError: 1243 pass 1244 1245 decimate.SetTargetReduction(1 - fraction) 1246 decimate.SetInputData(poly) 1247 decimate.Update() 1248 1249 self._update(decimate.GetOutput()) 1250 self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction() 1251 1252 self.pipeline = OperationNode( 1253 "decimate", 1254 parents=[self], 1255 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1256 ) 1257 return self 1258 1259 def decimate_pro( 1260 self, 1261 fraction=0.5, 1262 n=None, 1263 preserve_topology=True, 1264 preserve_boundaries=True, 1265 splitting=False, 1266 splitting_angle=75, 1267 feature_angle=0, 1268 inflection_point_ratio=10, 1269 vertex_degree=0, 1270 ) -> Self: 1271 """ 1272 Downsample the number of vertices in a mesh to `fraction`. 1273 1274 This filter preserves the `pointdata` of the input dataset. 1275 1276 Arguments: 1277 fraction : (float) 1278 The desired target of reduction. 1279 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1280 n : (int) 1281 the desired number of final points (`fraction` is recalculated based on it). 1282 preserve_topology : (bool) 1283 If on, mesh splitting and hole elimination will not occur. 1284 This may limit the maximum reduction that may be achieved. 1285 preserve_boundaries : (bool) 1286 Turn on/off the deletion of vertices on the boundary of a mesh. 1287 Control whether mesh boundaries are preserved during decimation. 1288 feature_angle : (float) 1289 Specify the angle that defines a feature. 1290 This angle is used to define what an edge is 1291 (i.e., if the surface normal between two adjacent triangles 1292 is >= FeatureAngle, an edge exists). 1293 splitting : (bool) 1294 Turn on/off the splitting of the mesh at corners, 1295 along edges, at non-manifold points, or anywhere else a split is required. 1296 Turning splitting off will better preserve the original topology of the mesh, 1297 but you may not obtain the requested reduction. 1298 splitting_angle : (float) 1299 Specify the angle that defines a sharp edge. 1300 This angle is used to control the splitting of the mesh. 1301 A split line exists when the surface normals between two edge connected triangles 1302 are >= `splitting_angle`. 1303 inflection_point_ratio : (float) 1304 An inflection point occurs when the ratio of reduction error between two iterations 1305 is greater than or equal to the `inflection_point_ratio` value. 1306 vertex_degree : (int) 1307 If the number of triangles connected to a vertex exceeds it then the vertex will be split. 1308 1309 Note: 1310 Setting `fraction=0.1` leaves 10% of the original number of vertices 1311 1312 See also: 1313 `decimate()` and `decimate_binned()`. 1314 """ 1315 poly = self.dataset 1316 if n: # N = desired number of points 1317 npt = poly.GetNumberOfPoints() 1318 fraction = n / npt 1319 if fraction >= 1: 1320 return self 1321 1322 decimate = vtki.new("DecimatePro") 1323 decimate.SetPreserveTopology(preserve_topology) 1324 decimate.SetBoundaryVertexDeletion(preserve_boundaries) 1325 if feature_angle: 1326 decimate.SetFeatureAngle(feature_angle) 1327 decimate.SetSplitting(splitting) 1328 decimate.SetSplitAngle(splitting_angle) 1329 decimate.SetInflectionPointRatio(inflection_point_ratio) 1330 if vertex_degree: 1331 decimate.SetDegree(vertex_degree) 1332 1333 decimate.SetTargetReduction(1 - fraction) 1334 decimate.SetInputData(poly) 1335 decimate.Update() 1336 self._update(decimate.GetOutput()) 1337 1338 self.pipeline = OperationNode( 1339 "decimate_pro", 1340 parents=[self], 1341 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1342 ) 1343 return self 1344 1345 def decimate_binned(self, divisions=(), use_clustering=False) -> Self: 1346 """ 1347 Downsample the number of vertices in a mesh. 1348 1349 This filter preserves the `celldata` of the input dataset, 1350 if `use_clustering=True` also the `pointdata` will be preserved in the result. 1351 1352 Arguments: 1353 divisions : (list) 1354 number of divisions along x, y and z axes. 1355 auto_adjust : (bool) 1356 if True, the number of divisions is automatically adjusted to 1357 create more uniform cells. 1358 use_clustering : (bool) 1359 use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html) 1360 instead of 1361 [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html). 1362 1363 See also: `decimate()` and `decimate_pro()`. 1364 """ 1365 if use_clustering: 1366 decimate = vtki.new("QuadricClustering") 1367 decimate.CopyCellDataOn() 1368 else: 1369 decimate = vtki.new("BinnedDecimation") 1370 decimate.ProducePointDataOn() 1371 decimate.ProduceCellDataOn() 1372 1373 decimate.SetInputData(self.dataset) 1374 1375 if len(divisions) == 0: 1376 decimate.SetAutoAdjustNumberOfDivisions(1) 1377 else: 1378 decimate.SetAutoAdjustNumberOfDivisions(0) 1379 decimate.SetNumberOfDivisions(divisions) 1380 decimate.Update() 1381 1382 self._update(decimate.GetOutput()) 1383 self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions() 1384 self.pipeline = OperationNode( 1385 "decimate_binned", 1386 parents=[self], 1387 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1388 ) 1389 return self 1390 1391 def generate_random_points(self, n: int, min_radius=0.0) -> "Points": 1392 """ 1393 Generate `n` uniformly distributed random points 1394 inside the polygonal mesh. 1395 1396 A new point data array is added to the output points 1397 called "OriginalCellID" which contains the index of 1398 the cell ID in which the point was generated. 1399 1400 Arguments: 1401 n : (int) 1402 number of points to generate. 1403 min_radius: (float) 1404 impose a minimum distance between points. 1405 If `min_radius` is set to 0, the points are 1406 generated uniformly at random inside the mesh. 1407 If `min_radius` is set to a positive value, 1408 the points are generated uniformly at random 1409 inside the mesh, but points closer than `min_radius` 1410 to any other point are discarded. 1411 1412 Returns a `vedo.Points` object. 1413 1414 Note: 1415 Consider using `points.probe(msh)` or 1416 `points.interpolate_data_from(msh)` 1417 to interpolate existing mesh data onto the new points. 1418 1419 Example: 1420 ```python 1421 from vedo import * 1422 msh = Mesh(dataurl + "panther.stl").lw(2) 1423 pts = msh.generate_random_points(20000, min_radius=0.5) 1424 print("Original cell ids:", pts.pointdata["OriginalCellID"]) 1425 show(pts, msh, axes=1).close() 1426 ``` 1427 """ 1428 cmesh = self.clone().clean().triangulate().compute_cell_size() 1429 triangles = cmesh.cells 1430 vertices = cmesh.vertices 1431 cumul = np.cumsum(cmesh.celldata["Area"]) 1432 1433 out_pts = [] 1434 orig_cell = [] 1435 for _ in range(n): 1436 # choose a triangle based on area 1437 random_area = np.random.random() * cumul[-1] 1438 it = np.searchsorted(cumul, random_area) 1439 A, B, C = vertices[triangles[it]] 1440 # calculate the random point in the triangle 1441 r1, r2 = np.random.random(2) 1442 if r1 + r2 > 1: 1443 r1 = 1 - r1 1444 r2 = 1 - r2 1445 out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C) 1446 orig_cell.append(it) 1447 nporig_cell = np.array(orig_cell, dtype=np.uint32) 1448 1449 vpts = Points(out_pts) 1450 vpts.pointdata["OriginalCellID"] = nporig_cell 1451 1452 if min_radius > 0: 1453 vpts.subsample(min_radius, absolute=True) 1454 1455 vpts.point_size(5).color("k1") 1456 vpts.name = "RandomPoints" 1457 vpts.pipeline = OperationNode( 1458 "generate_random_points", c="#edabab", parents=[self]) 1459 return vpts 1460 1461 def delete_cells(self, ids: List[int]) -> Self: 1462 """ 1463 Remove cells from the mesh object by their ID. 1464 Points (vertices) are not removed (you may use `clean()` to remove those). 1465 """ 1466 self.dataset.BuildLinks() 1467 for cid in ids: 1468 self.dataset.DeleteCell(cid) 1469 self.dataset.RemoveDeletedCells() 1470 self.dataset.Modified() 1471 self.mapper.Modified() 1472 self.pipeline = OperationNode( 1473 "delete_cells", 1474 parents=[self], 1475 comment=f"#cells {self.dataset.GetNumberOfCells()}", 1476 ) 1477 return self 1478 1479 def delete_cells_by_point_index(self, indices: List[int]) -> Self: 1480 """ 1481 Delete a list of vertices identified by any of their vertex index. 1482 1483 See also `delete_cells()`. 1484 1485 Examples: 1486 - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py) 1487 1488 ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png) 1489 """ 1490 cell_ids = vtki.vtkIdList() 1491 self.dataset.BuildLinks() 1492 n = 0 1493 for i in np.unique(indices): 1494 self.dataset.GetPointCells(i, cell_ids) 1495 for j in range(cell_ids.GetNumberOfIds()): 1496 self.dataset.DeleteCell(cell_ids.GetId(j)) # flag cell 1497 n += 1 1498 1499 self.dataset.RemoveDeletedCells() 1500 self.dataset.Modified() 1501 self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self]) 1502 return self 1503 1504 def collapse_edges(self, distance: float, iterations=1) -> Self: 1505 """ 1506 Collapse mesh edges so that are all above `distance`. 1507 1508 Example: 1509 ```python 1510 from vedo import * 1511 np.random.seed(2) 1512 grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1) 1513 grid1.celldata['scalar'] = grid1.cell_centers[:,1] 1514 grid2 = grid1.clone().collapse_edges(0.1) 1515 show(grid1, grid2, N=2, axes=1) 1516 ``` 1517 """ 1518 for _ in range(iterations): 1519 medges = self.edges 1520 pts = self.vertices 1521 newpts = np.array(pts) 1522 moved = [] 1523 for e in medges: 1524 if len(e) == 2: 1525 id0, id1 = e 1526 p0, p1 = pts[id0], pts[id1] 1527 if (np.linalg.norm(p1-p0) < distance 1528 and id0 not in moved 1529 and id1 not in moved 1530 ): 1531 p = (p0 + p1) / 2 1532 newpts[id0] = p 1533 newpts[id1] = p 1534 moved += [id0, id1] 1535 self.vertices = newpts 1536 cpd = vtki.new("CleanPolyData") 1537 cpd.ConvertLinesToPointsOff() 1538 cpd.ConvertPolysToLinesOff() 1539 cpd.ConvertStripsToPolysOff() 1540 cpd.SetInputData(self.dataset) 1541 cpd.Update() 1542 self._update(cpd.GetOutput()) 1543 1544 self.pipeline = OperationNode( 1545 "collapse_edges", 1546 parents=[self], 1547 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1548 ) 1549 return self 1550 1551 def adjacency_list(self) -> List[set]: 1552 """ 1553 Computes the adjacency list for mesh edge-graph. 1554 1555 Returns: 1556 a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex 1557 """ 1558 inc = [set()] * self.nvertices 1559 for cell in self.cells: 1560 nc = len(cell) 1561 if nc > 1: 1562 for i in range(nc-1): 1563 ci = cell[i] 1564 inc[ci] = inc[ci].union({cell[i-1], cell[i+1]}) 1565 return inc 1566 1567 def graph_ball(self, index, n: int) -> set: 1568 """ 1569 Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`. 1570 1571 Arguments: 1572 index : (int) 1573 index of the vertex 1574 n : (int) 1575 radius in the graph metric 1576 1577 Returns: 1578 the set of indices of the vertices which are at most `n` edges from vertex `index`. 1579 """ 1580 if n == 0: 1581 return {index} 1582 else: 1583 al = self.adjacency_list() 1584 ball = {index} 1585 i = 0 1586 while i < n and len(ball) < self.nvertices: 1587 for v in ball: 1588 ball = ball.union(al[v]) 1589 i += 1 1590 return ball 1591 1592 def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self: 1593 """ 1594 Adjust mesh point positions using the so-called "Windowed Sinc" method. 1595 1596 Arguments: 1597 niter : (int) 1598 number of iterations. 1599 pass_band : (float) 1600 set the pass_band value for the windowed sinc filter. 1601 edge_angle : (float) 1602 edge angle to control smoothing along edges (either interior or boundary). 1603 feature_angle : (float) 1604 specifies the feature angle for sharp edge identification. 1605 boundary : (bool) 1606 specify if boundary should also be smoothed or kept unmodified 1607 1608 Examples: 1609 - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py) 1610 1611 ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png) 1612 """ 1613 cl = vtki.new("CleanPolyData") 1614 cl.SetInputData(self.dataset) 1615 cl.Update() 1616 smf = vtki.new("WindowedSincPolyDataFilter") 1617 smf.SetInputData(cl.GetOutput()) 1618 smf.SetNumberOfIterations(niter) 1619 smf.SetEdgeAngle(edge_angle) 1620 smf.SetFeatureAngle(feature_angle) 1621 smf.SetPassBand(pass_band) 1622 smf.NormalizeCoordinatesOn() 1623 smf.NonManifoldSmoothingOn() 1624 smf.FeatureEdgeSmoothingOn() 1625 smf.SetBoundarySmoothing(boundary) 1626 smf.Update() 1627 1628 self._update(smf.GetOutput()) 1629 1630 self.pipeline = OperationNode( 1631 "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1632 ) 1633 return self 1634 1635 def fill_holes(self, size=None) -> Self: 1636 """ 1637 Identifies and fills holes in the input mesh. 1638 Holes are identified by locating boundary edges, linking them together 1639 into loops, and then triangulating the resulting loops. 1640 1641 Arguments: 1642 size : (float) 1643 Approximate limit to the size of the hole that can be filled. 1644 1645 Examples: 1646 - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py) 1647 """ 1648 fh = vtki.new("FillHolesFilter") 1649 if not size: 1650 mb = self.diagonal_size() 1651 size = mb / 10 1652 fh.SetHoleSize(size) 1653 fh.SetInputData(self.dataset) 1654 fh.Update() 1655 1656 self._update(fh.GetOutput()) 1657 1658 self.pipeline = OperationNode( 1659 "fill_holes", 1660 parents=[self], 1661 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1662 ) 1663 return self 1664 1665 def contains(self, point: tuple, tol=1e-05) -> bool: 1666 """ 1667 Return True if point is inside a polydata closed surface. 1668 1669 Note: 1670 if you have many points to check use `inside_points()` instead. 1671 1672 Example: 1673 ```python 1674 from vedo import * 1675 s = Sphere().c('green5').alpha(0.5) 1676 pt = [0.1, 0.2, 0.3] 1677 print("Sphere contains", pt, s.contains(pt)) 1678 show(s, Point(pt), axes=1).close() 1679 ``` 1680 """ 1681 points = vtki.vtkPoints() 1682 points.InsertNextPoint(point) 1683 poly = vtki.vtkPolyData() 1684 poly.SetPoints(points) 1685 sep = vtki.new("SelectEnclosedPoints") 1686 sep.SetTolerance(tol) 1687 sep.CheckSurfaceOff() 1688 sep.SetInputData(poly) 1689 sep.SetSurfaceData(self.dataset) 1690 sep.Update() 1691 return bool(sep.IsInside(0)) 1692 1693 def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]: 1694 """ 1695 Return the point cloud that is inside mesh surface as a new Points object. 1696 1697 If return_ids is True a list of IDs is returned and in addition input points 1698 are marked by a pointdata array named "IsInside". 1699 1700 Example: 1701 `print(pts.pointdata["IsInside"])` 1702 1703 Examples: 1704 - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 1705 1706 ![](https://vedo.embl.es/images/basic/pca.png) 1707 """ 1708 if isinstance(pts, Points): 1709 poly = pts.dataset 1710 ptsa = pts.vertices 1711 else: 1712 ptsa = np.asarray(pts) 1713 vpoints = vtki.vtkPoints() 1714 vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32)) 1715 poly = vtki.vtkPolyData() 1716 poly.SetPoints(vpoints) 1717 1718 sep = vtki.new("SelectEnclosedPoints") 1719 # sep = vtki.new("ExtractEnclosedPoints() 1720 sep.SetTolerance(tol) 1721 sep.SetInputData(poly) 1722 sep.SetSurfaceData(self.dataset) 1723 sep.SetInsideOut(invert) 1724 sep.Update() 1725 1726 varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints") 1727 mask = vtk2numpy(varr).astype(bool) 1728 ids = np.array(range(len(ptsa)), dtype=int)[mask] 1729 1730 if isinstance(pts, Points): 1731 varr.SetName("IsInside") 1732 pts.dataset.GetPointData().AddArray(varr) 1733 1734 if return_ids: 1735 return ids 1736 1737 pcl = Points(ptsa[ids]) 1738 pcl.name = "InsidePoints" 1739 1740 pcl.pipeline = OperationNode( 1741 "inside_points", 1742 parents=[self, ptsa], 1743 comment=f"#pts {pcl.dataset.GetNumberOfPoints()}", 1744 ) 1745 return pcl 1746 1747 def boundaries( 1748 self, 1749 boundary_edges=True, 1750 manifold_edges=False, 1751 non_manifold_edges=False, 1752 feature_angle=None, 1753 return_point_ids=False, 1754 return_cell_ids=False, 1755 cell_edge=False, 1756 ) -> Union[Self, np.ndarray]: 1757 """ 1758 Return the boundary lines of an input mesh. 1759 Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method. 1760 1761 Arguments: 1762 boundary_edges : (bool) 1763 Turn on/off the extraction of boundary edges. 1764 manifold_edges : (bool) 1765 Turn on/off the extraction of manifold edges. 1766 non_manifold_edges : (bool) 1767 Turn on/off the extraction of non-manifold edges. 1768 feature_angle : (bool) 1769 Specify the min angle btw 2 faces for extracting edges. 1770 return_point_ids : (bool) 1771 return a numpy array of point indices 1772 return_cell_ids : (bool) 1773 return a numpy array of cell indices 1774 cell_edge : (bool) 1775 set to `True` if a cell need to share an edge with 1776 the boundary line, or `False` if a single vertex is enough 1777 1778 Examples: 1779 - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py) 1780 1781 ![](https://vedo.embl.es/images/basic/boundaries.png) 1782 """ 1783 fe = vtki.new("FeatureEdges") 1784 fe.SetBoundaryEdges(boundary_edges) 1785 fe.SetNonManifoldEdges(non_manifold_edges) 1786 fe.SetManifoldEdges(manifold_edges) 1787 try: 1788 fe.SetPassLines(True) # vtk9.2 1789 except AttributeError: 1790 pass 1791 fe.ColoringOff() 1792 fe.SetFeatureEdges(False) 1793 if feature_angle is not None: 1794 fe.SetFeatureEdges(True) 1795 fe.SetFeatureAngle(feature_angle) 1796 1797 if return_point_ids or return_cell_ids: 1798 idf = vtki.new("IdFilter") 1799 idf.SetInputData(self.dataset) 1800 idf.SetPointIdsArrayName("BoundaryIds") 1801 idf.SetPointIds(True) 1802 idf.Update() 1803 1804 fe.SetInputData(idf.GetOutput()) 1805 fe.Update() 1806 1807 vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds") 1808 npid = vtk2numpy(vid).astype(int) 1809 1810 if return_point_ids: 1811 return npid 1812 1813 if return_cell_ids: 1814 n = 1 if cell_edge else 0 1815 inface = [] 1816 for i, face in enumerate(self.cells): 1817 # isin = np.any([vtx in npid for vtx in face]) 1818 isin = 0 1819 for vtx in face: 1820 isin += int(vtx in npid) 1821 if isin > n: 1822 break 1823 if isin > n: 1824 inface.append(i) 1825 return np.array(inface).astype(int) 1826 1827 return self 1828 1829 else: 1830 1831 fe.SetInputData(self.dataset) 1832 fe.Update() 1833 msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off") 1834 msh.name = "MeshBoundaries" 1835 1836 msh.pipeline = OperationNode( 1837 "boundaries", 1838 parents=[self], 1839 shape="octagon", 1840 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 1841 ) 1842 return msh 1843 1844 def imprint(self, loopline, tol=0.01) -> Self: 1845 """ 1846 Imprint the contact surface of one object onto another surface. 1847 1848 Arguments: 1849 loopline : (vedo.Line) 1850 a Line object to be imprinted onto the mesh. 1851 tol : (float) 1852 projection tolerance which controls how close the imprint 1853 surface must be to the target. 1854 1855 Example: 1856 ```python 1857 from vedo import * 1858 grid = Grid()#.triangulate() 1859 circle = Circle(r=0.3, res=24).pos(0.11,0.12) 1860 line = Line(circle, closed=True, lw=4, c='r4') 1861 grid.imprint(line) 1862 show(grid, line, axes=1).close() 1863 ``` 1864 ![](https://vedo.embl.es/images/feats/imprint.png) 1865 """ 1866 loop = vtki.new("ContourLoopExtraction") 1867 loop.SetInputData(loopline.dataset) 1868 loop.Update() 1869 1870 clean_loop = vtki.new("CleanPolyData") 1871 clean_loop.SetInputData(loop.GetOutput()) 1872 clean_loop.Update() 1873 1874 imp = vtki.new("ImprintFilter") 1875 imp.SetTargetData(self.dataset) 1876 imp.SetImprintData(clean_loop.GetOutput()) 1877 imp.SetTolerance(tol) 1878 imp.BoundaryEdgeInsertionOn() 1879 imp.TriangulateOutputOn() 1880 imp.Update() 1881 1882 self._update(imp.GetOutput()) 1883 1884 self.pipeline = OperationNode( 1885 "imprint", 1886 parents=[self], 1887 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1888 ) 1889 return self 1890 1891 def connected_vertices(self, index: int) -> List[int]: 1892 """Find all vertices connected to an input vertex specified by its index. 1893 1894 Examples: 1895 - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py) 1896 1897 ![](https://vedo.embl.es/images/basic/connVtx.png) 1898 """ 1899 poly = self.dataset 1900 1901 cell_idlist = vtki.vtkIdList() 1902 poly.GetPointCells(index, cell_idlist) 1903 1904 idxs = [] 1905 for i in range(cell_idlist.GetNumberOfIds()): 1906 point_idlist = vtki.vtkIdList() 1907 poly.GetCellPoints(cell_idlist.GetId(i), point_idlist) 1908 for j in range(point_idlist.GetNumberOfIds()): 1909 idj = point_idlist.GetId(j) 1910 if idj == index: 1911 continue 1912 if idj in idxs: 1913 continue 1914 idxs.append(idj) 1915 1916 return idxs 1917 1918 def extract_cells(self, ids: List[int]) -> Self: 1919 """ 1920 Extract a subset of cells from a mesh and return it as a new mesh. 1921 """ 1922 selectCells = vtki.new("SelectionNode") 1923 selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL) 1924 selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES) 1925 idarr = vtki.vtkIdTypeArray() 1926 idarr.SetNumberOfComponents(1) 1927 idarr.SetNumberOfValues(len(ids)) 1928 for i, v in enumerate(ids): 1929 idarr.SetValue(i, v) 1930 selectCells.SetSelectionList(idarr) 1931 1932 selection = vtki.new("Selection") 1933 selection.AddNode(selectCells) 1934 1935 extractSelection = vtki.new("ExtractSelection") 1936 extractSelection.SetInputData(0, self.dataset) 1937 extractSelection.SetInputData(1, selection) 1938 extractSelection.Update() 1939 1940 gf = vtki.new("GeometryFilter") 1941 gf.SetInputData(extractSelection.GetOutput()) 1942 gf.Update() 1943 msh = Mesh(gf.GetOutput()) 1944 msh.copy_properties_from(self) 1945 return msh 1946 1947 def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]: 1948 """Find all cellls connected to an input vertex specified by its index.""" 1949 1950 # Find all cells connected to point index 1951 dpoly = self.dataset 1952 idlist = vtki.vtkIdList() 1953 dpoly.GetPointCells(index, idlist) 1954 1955 ids = vtki.vtkIdTypeArray() 1956 ids.SetNumberOfComponents(1) 1957 rids = [] 1958 for k in range(idlist.GetNumberOfIds()): 1959 cid = idlist.GetId(k) 1960 ids.InsertNextValue(cid) 1961 rids.append(int(cid)) 1962 if return_ids: 1963 return rids 1964 1965 selection_node = vtki.new("SelectionNode") 1966 selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL) 1967 selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES) 1968 selection_node.SetSelectionList(ids) 1969 selection = vtki.new("Selection") 1970 selection.AddNode(selection_node) 1971 extractSelection = vtki.new("ExtractSelection") 1972 extractSelection.SetInputData(0, dpoly) 1973 extractSelection.SetInputData(1, selection) 1974 extractSelection.Update() 1975 gf = vtki.new("GeometryFilter") 1976 gf.SetInputData(extractSelection.GetOutput()) 1977 gf.Update() 1978 return Mesh(gf.GetOutput()).lw(1) 1979 1980 def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self: 1981 """ 1982 Return a new line `Mesh` which corresponds to the outer `silhouette` 1983 of the input as seen along a specified `direction`, this can also be 1984 a `vtkCamera` object. 1985 1986 Arguments: 1987 direction : (list) 1988 viewpoint direction vector. 1989 If `None` this is guessed by looking at the minimum 1990 of the sides of the bounding box. 1991 border_edges : (bool) 1992 enable or disable generation of border edges 1993 feature_angle : (float) 1994 minimal angle for sharp edges detection. 1995 If set to `False` the functionality is disabled. 1996 1997 Examples: 1998 - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py) 1999 2000 ![](https://vedo.embl.es/images/basic/silhouette1.png) 2001 """ 2002 sil = vtki.new("PolyDataSilhouette") 2003 sil.SetInputData(self.dataset) 2004 sil.SetBorderEdges(border_edges) 2005 if feature_angle is False: 2006 sil.SetEnableFeatureAngle(0) 2007 else: 2008 sil.SetEnableFeatureAngle(1) 2009 sil.SetFeatureAngle(feature_angle) 2010 2011 if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera: 2012 sil.SetCamera(vedo.plotter_instance.camera) 2013 m = Mesh() 2014 m.mapper.SetInputConnection(sil.GetOutputPort()) 2015 2016 elif isinstance(direction, vtki.vtkCamera): 2017 sil.SetCamera(direction) 2018 m = Mesh() 2019 m.mapper.SetInputConnection(sil.GetOutputPort()) 2020 2021 elif direction == "2d": 2022 sil.SetVector(3.4, 4.5, 5.6) # random 2023 sil.SetDirectionToSpecifiedVector() 2024 sil.Update() 2025 m = Mesh(sil.GetOutput()) 2026 2027 elif is_sequence(direction): 2028 sil.SetVector(direction) 2029 sil.SetDirectionToSpecifiedVector() 2030 sil.Update() 2031 m = Mesh(sil.GetOutput()) 2032 else: 2033 vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}") 2034 vedo.logger.error("first render the scene with show() or specify camera/direction") 2035 return self 2036 2037 m.lw(2).c((0, 0, 0)).lighting("off") 2038 m.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2039 m.pipeline = OperationNode("silhouette", parents=[self]) 2040 m.name = "Silhouette" 2041 return m 2042 2043 def isobands(self, n=10, vmin=None, vmax=None) -> Self: 2044 """ 2045 Return a new `Mesh` representing the isobands of the active scalars. 2046 This is a new mesh where the scalar is now associated to cell faces and 2047 used to colorize the mesh. 2048 2049 Arguments: 2050 n : (int) 2051 number of isobands in the range 2052 vmin : (float) 2053 minimum of the range 2054 vmax : (float) 2055 maximum of the range 2056 2057 Examples: 2058 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2059 """ 2060 r0, r1 = self.dataset.GetScalarRange() 2061 if vmin is None: 2062 vmin = r0 2063 if vmax is None: 2064 vmax = r1 2065 2066 # -------------------------------- 2067 bands = [] 2068 dx = (vmax - vmin) / float(n) 2069 b = [vmin, vmin + dx / 2.0, vmin + dx] 2070 i = 0 2071 while i < n: 2072 bands.append(b) 2073 b = [b[0] + dx, b[1] + dx, b[2] + dx] 2074 i += 1 2075 2076 # annotate, use the midpoint of the band as the label 2077 lut = self.mapper.GetLookupTable() 2078 labels = [] 2079 for b in bands: 2080 labels.append("{:4.2f}".format(b[1])) 2081 values = vtki.vtkVariantArray() 2082 for la in labels: 2083 values.InsertNextValue(vtki.vtkVariant(la)) 2084 for i in range(values.GetNumberOfTuples()): 2085 lut.SetAnnotation(i, values.GetValue(i).ToString()) 2086 2087 bcf = vtki.new("BandedPolyDataContourFilter") 2088 bcf.SetInputData(self.dataset) 2089 # Use either the minimum or maximum value for each band. 2090 for i, band in enumerate(bands): 2091 bcf.SetValue(i, band[2]) 2092 # We will use an indexed lookup table. 2093 bcf.SetScalarModeToIndex() 2094 bcf.GenerateContourEdgesOff() 2095 bcf.Update() 2096 bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands") 2097 2098 m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True) 2099 m1.mapper.SetLookupTable(lut) 2100 m1.mapper.SetScalarRange(lut.GetRange()) 2101 m1.pipeline = OperationNode("isobands", parents=[self]) 2102 m1.name = "IsoBands" 2103 return m1 2104 2105 def isolines(self, n=10, vmin=None, vmax=None) -> Self: 2106 """ 2107 Return a new `Mesh` representing the isolines of the active scalars. 2108 2109 Arguments: 2110 n : (int) 2111 number of isolines in the range 2112 vmin : (float) 2113 minimum of the range 2114 vmax : (float) 2115 maximum of the range 2116 2117 Examples: 2118 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2119 2120 ![](https://vedo.embl.es/images/pyplot/isolines.png) 2121 """ 2122 bcf = vtki.new("ContourFilter") 2123 bcf.SetInputData(self.dataset) 2124 r0, r1 = self.dataset.GetScalarRange() 2125 if vmin is None: 2126 vmin = r0 2127 if vmax is None: 2128 vmax = r1 2129 bcf.GenerateValues(n, vmin, vmax) 2130 bcf.Update() 2131 sf = vtki.new("Stripper") 2132 sf.SetJoinContiguousSegments(True) 2133 sf.SetInputData(bcf.GetOutput()) 2134 sf.Update() 2135 cl = vtki.new("CleanPolyData") 2136 cl.SetInputData(sf.GetOutput()) 2137 cl.Update() 2138 msh = Mesh(cl.GetOutput(), c="k").lighting("off") 2139 msh.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2140 msh.pipeline = OperationNode("isolines", parents=[self]) 2141 msh.name = "IsoLines" 2142 return msh 2143 2144 def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self: 2145 """ 2146 Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices. 2147 The input dataset is swept around the z-axis to create new polygonal primitives. 2148 For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus. 2149 2150 You can control whether the sweep of a 2D object (i.e., polygon or triangle strip) 2151 is capped with the generating geometry. 2152 Also, you can control the angle of rotation, and whether translation along the z-axis 2153 is performed along with the rotation. (Translation is useful for creating "springs"). 2154 You also can adjust the radius of the generating geometry using the "dR" keyword. 2155 2156 The skirt is generated by locating certain topological features. 2157 Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips) 2158 generate surfaces. This is true also of lines or polylines. Vertices generate lines. 2159 2160 This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses; 2161 or translational/rotational symmetric objects like springs or corkscrews. 2162 2163 Arguments: 2164 zshift : (float) 2165 shift along z axis. 2166 direction : (list) 2167 extrusion direction in the xy plane. 2168 note that zshift is forced to be the 3rd component of direction, 2169 which is therefore ignored. 2170 rotation : (float) 2171 set the angle of rotation. 2172 dr : (float) 2173 set the radius variation in absolute units. 2174 cap : (bool) 2175 enable or disable capping. 2176 res : (int) 2177 set the resolution of the generating geometry. 2178 2179 Warning: 2180 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result 2181 in two separate surfaces if capping is on, or no surface if capping is off. 2182 2183 Examples: 2184 - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py) 2185 2186 ![](https://vedo.embl.es/images/basic/extrude.png) 2187 """ 2188 rf = vtki.new("RotationalExtrusionFilter") 2189 # rf = vtki.new("LinearExtrusionFilter") 2190 rf.SetInputData(self.dataset) # must not be transformed 2191 rf.SetResolution(res) 2192 rf.SetCapping(cap) 2193 rf.SetAngle(rotation) 2194 rf.SetTranslation(zshift) 2195 rf.SetDeltaRadius(dr) 2196 rf.Update() 2197 2198 # convert triangle strips to polygonal data 2199 tris = vtki.new("TriangleFilter") 2200 tris.SetInputData(rf.GetOutput()) 2201 tris.Update() 2202 2203 m = Mesh(tris.GetOutput()) 2204 2205 if len(direction) > 1: 2206 p = self.pos() 2207 LT = vedo.LinearTransform() 2208 LT.translate(-p) 2209 LT.concatenate([ 2210 [1, 0, direction[0]], 2211 [0, 1, direction[1]], 2212 [0, 0, 1] 2213 ]) 2214 LT.translate(p) 2215 m.apply_transform(LT) 2216 2217 m.copy_properties_from(self).flat().lighting("default") 2218 m.pipeline = OperationNode( 2219 "extrude", parents=[self], 2220 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2221 ) 2222 m.name = "ExtrudedMesh" 2223 return m 2224 2225 def extrude_and_trim_with( 2226 self, 2227 surface: "Mesh", 2228 direction=(), 2229 strategy="all", 2230 cap=True, 2231 cap_strategy="max", 2232 ) -> Self: 2233 """ 2234 Extrude a Mesh and trim it with an input surface mesh. 2235 2236 Arguments: 2237 surface : (Mesh) 2238 the surface mesh to trim with. 2239 direction : (list) 2240 extrusion direction in the xy plane. 2241 strategy : (str) 2242 either "boundary_edges" or "all_edges". 2243 cap : (bool) 2244 enable or disable capping. 2245 cap_strategy : (str) 2246 either "intersection", "minimum_distance", "maximum_distance", "average_distance". 2247 2248 The input Mesh is swept along a specified direction forming a "skirt" 2249 from the boundary edges 2D primitives (i.e., edges used by only one polygon); 2250 and/or from vertices and lines. 2251 The extent of the sweeping is limited by a second input: defined where 2252 the sweep intersects a user-specified surface. 2253 2254 Capping of the extrusion can be enabled. 2255 In this case the input, generating primitive is copied inplace as well 2256 as to the end of the extrusion skirt. 2257 (See warnings below on what happens if the intersecting sweep does not 2258 intersect, or partially intersects the trim surface.) 2259 2260 Note that this method operates in two fundamentally different modes 2261 based on the extrusion strategy. 2262 If the strategy is "boundary_edges", then only the boundary edges of the input's 2263 2D primitives are extruded (verts and lines are extruded to generate lines and quads). 2264 However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives 2265 is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads). 2266 2267 Warning: 2268 The extrusion direction is assumed to define an infinite line. 2269 The intersection with the trim surface is along a ray from the - to + direction, 2270 however only the first intersection is taken. 2271 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate 2272 surfaces if capping is on and "boundary_edges" enabled, 2273 or no surface if capping is off and "boundary_edges" is enabled. 2274 If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, 2275 then no output for that primitive will be generated. In extreme cases, it is possible that no output 2276 whatsoever will be generated. 2277 2278 Example: 2279 ```python 2280 from vedo import * 2281 sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5') 2282 circle = Circle([0,0,0], r=2, res=100).color('b6') 2283 extruded_circle = circle.extrude_and_trim_with( 2284 sphere, 2285 direction=[0,-0.2,1], 2286 strategy="bound", 2287 cap=True, 2288 cap_strategy="intersection", 2289 ) 2290 circle.lw(3).color("tomato").shift(dz=-0.1) 2291 show(circle, sphere, extruded_circle, axes=1).close() 2292 ``` 2293 """ 2294 trimmer = vtki.new("TrimmedExtrusionFilter") 2295 trimmer.SetInputData(self.dataset) 2296 trimmer.SetCapping(cap) 2297 trimmer.SetExtrusionDirection(direction) 2298 trimmer.SetTrimSurfaceData(surface.dataset) 2299 if "bound" in strategy: 2300 trimmer.SetExtrusionStrategyToBoundaryEdges() 2301 elif "all" in strategy: 2302 trimmer.SetExtrusionStrategyToAllEdges() 2303 else: 2304 vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}") 2305 # print (trimmer.GetExtrusionStrategy()) 2306 2307 if "intersect" in cap_strategy: 2308 trimmer.SetCappingStrategyToIntersection() 2309 elif "min" in cap_strategy: 2310 trimmer.SetCappingStrategyToMinimumDistance() 2311 elif "max" in cap_strategy: 2312 trimmer.SetCappingStrategyToMaximumDistance() 2313 elif "ave" in cap_strategy: 2314 trimmer.SetCappingStrategyToAverageDistance() 2315 else: 2316 vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}") 2317 # print (trimmer.GetCappingStrategy()) 2318 2319 trimmer.Update() 2320 2321 m = Mesh(trimmer.GetOutput()) 2322 m.copy_properties_from(self).flat().lighting("default") 2323 m.pipeline = OperationNode( 2324 "extrude_and_trim", parents=[self, surface], 2325 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2326 ) 2327 m.name = "ExtrudedAndTrimmedMesh" 2328 return m 2329 2330 def split( 2331 self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True 2332 ) -> List[Self]: 2333 """ 2334 Split a mesh by connectivity and order the pieces by increasing area. 2335 2336 Arguments: 2337 maxdepth : (int) 2338 only consider this maximum number of mesh parts. 2339 flag : (bool) 2340 if set to True return the same single object, 2341 but add a "RegionId" array to flag the mesh subparts 2342 must_share_edge : (bool) 2343 if True, mesh regions that only share single points will be split. 2344 sort_by_area : (bool) 2345 if True, sort the mesh parts by decreasing area. 2346 2347 Examples: 2348 - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py) 2349 2350 ![](https://vedo.embl.es/images/advanced/splitmesh.png) 2351 """ 2352 pd = self.dataset 2353 if must_share_edge: 2354 if pd.GetNumberOfPolys() == 0: 2355 vedo.logger.warning("in split(): no polygons found. Skip.") 2356 return [self] 2357 cf = vtki.new("PolyDataEdgeConnectivityFilter") 2358 cf.BarrierEdgesOff() 2359 else: 2360 cf = vtki.new("PolyDataConnectivityFilter") 2361 2362 cf.SetInputData(pd) 2363 cf.SetExtractionModeToAllRegions() 2364 cf.SetColorRegions(True) 2365 cf.Update() 2366 out = cf.GetOutput() 2367 2368 if not out.GetNumberOfPoints(): 2369 return [self] 2370 2371 if flag: 2372 self.pipeline = OperationNode("split mesh", parents=[self]) 2373 self._update(out) 2374 return [self] 2375 2376 msh = Mesh(out) 2377 if must_share_edge: 2378 arr = msh.celldata["RegionId"] 2379 on = "cells" 2380 else: 2381 arr = msh.pointdata["RegionId"] 2382 on = "points" 2383 2384 alist = [] 2385 for t in range(max(arr) + 1): 2386 if t == maxdepth: 2387 break 2388 suba = msh.clone().threshold("RegionId", t, t, on=on) 2389 if sort_by_area: 2390 area = suba.area() 2391 else: 2392 area = 0 # dummy 2393 suba.name = "MeshRegion" + str(t) 2394 alist.append([suba, area]) 2395 2396 if sort_by_area: 2397 alist.sort(key=lambda x: x[1]) 2398 alist.reverse() 2399 2400 blist = [] 2401 for i, l in enumerate(alist): 2402 l[0].color(i + 1).phong() 2403 l[0].mapper.ScalarVisibilityOff() 2404 blist.append(l[0]) 2405 if i < 10: 2406 l[0].pipeline = OperationNode( 2407 f"split mesh {i}", 2408 parents=[self], 2409 comment=f"#pts {l[0].dataset.GetNumberOfPoints()}", 2410 ) 2411 return blist 2412 2413 def extract_largest_region(self) -> Self: 2414 """ 2415 Extract the largest connected part of a mesh and discard all the smaller pieces. 2416 2417 Examples: 2418 - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py) 2419 """ 2420 conn = vtki.new("PolyDataConnectivityFilter") 2421 conn.SetExtractionModeToLargestRegion() 2422 conn.ScalarConnectivityOff() 2423 conn.SetInputData(self.dataset) 2424 conn.Update() 2425 2426 m = Mesh(conn.GetOutput()) 2427 m.copy_properties_from(self) 2428 m.pipeline = OperationNode( 2429 "extract_largest_region", 2430 parents=[self], 2431 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2432 ) 2433 m.name = "MeshLargestRegion" 2434 return m 2435 2436 def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self: 2437 """Volumetric union, intersection and subtraction of surfaces. 2438 2439 Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`. 2440 2441 Two possible algorithms are available. 2442 Setting `method` to 0 (the default) uses the boolean operation algorithm 2443 written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01); 2444 setting `method` to 1 will use the "loop" boolean algorithm 2445 written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015). 2446 2447 Use `tol` to specify the absolute tolerance used to determine 2448 when the distance between two points is considered to be zero (defaults to 1e-6). 2449 2450 Example: 2451 - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py) 2452 2453 ![](https://vedo.embl.es/images/basic/boolean.png) 2454 """ 2455 if method == 0: 2456 bf = vtki.new("BooleanOperationPolyDataFilter") 2457 elif method == 1: 2458 bf = vtki.new("LoopBooleanPolyDataFilter") 2459 else: 2460 raise ValueError(f"Unknown method={method}") 2461 2462 poly1 = self.compute_normals().dataset 2463 poly2 = mesh2.compute_normals().dataset 2464 2465 if operation.lower() in ("plus", "+"): 2466 bf.SetOperationToUnion() 2467 elif operation.lower() == "intersect": 2468 bf.SetOperationToIntersection() 2469 elif operation.lower() in ("minus", "-"): 2470 bf.SetOperationToDifference() 2471 2472 if tol: 2473 bf.SetTolerance(tol) 2474 2475 bf.SetInputData(0, poly1) 2476 bf.SetInputData(1, poly2) 2477 bf.Update() 2478 2479 msh = Mesh(bf.GetOutput(), c=None) 2480 msh.flat() 2481 2482 msh.pipeline = OperationNode( 2483 "boolean " + operation, 2484 parents=[self, mesh2], 2485 shape="cylinder", 2486 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2487 ) 2488 msh.name = self.name + operation + mesh2.name 2489 return msh 2490 2491 def intersect_with(self, mesh2, tol=1e-06) -> Self: 2492 """ 2493 Intersect this Mesh with the input surface to return a set of lines. 2494 2495 Examples: 2496 - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py) 2497 2498 ![](https://vedo.embl.es/images/basic/surfIntersect.png) 2499 """ 2500 bf = vtki.new("IntersectionPolyDataFilter") 2501 bf.SetGlobalWarningDisplay(0) 2502 bf.SetTolerance(tol) 2503 bf.SetInputData(0, self.dataset) 2504 bf.SetInputData(1, mesh2.dataset) 2505 bf.Update() 2506 msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off") 2507 msh.properties.SetLineWidth(3) 2508 msh.pipeline = OperationNode( 2509 "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}" 2510 ) 2511 msh.name = "SurfaceIntersection" 2512 return msh 2513 2514 def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: 2515 """ 2516 Return the list of points intersecting the mesh 2517 along the segment defined by two points `p0` and `p1`. 2518 2519 Use `return_ids` to return the cell ids along with point coords 2520 2521 Example: 2522 ```python 2523 from vedo import * 2524 s = Spring() 2525 pts = s.intersect_with_line([0,0,0], [1,0.1,0]) 2526 ln = Line([0,0,0], [1,0.1,0], c='blue') 2527 ps = Points(pts, r=10, c='r') 2528 show(s, ln, ps, bg='white').close() 2529 ``` 2530 ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png) 2531 """ 2532 if isinstance(p0, Points): 2533 p0, p1 = p0.vertices 2534 2535 if not self.line_locator: 2536 self.line_locator = vtki.new("OBBTree") 2537 self.line_locator.SetDataSet(self.dataset) 2538 if not tol: 2539 tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000 2540 self.line_locator.SetTolerance(tol) 2541 self.line_locator.BuildLocator() 2542 2543 vpts = vtki.vtkPoints() 2544 idlist = vtki.vtkIdList() 2545 self.line_locator.IntersectWithLine(p0, p1, vpts, idlist) 2546 pts = [] 2547 for i in range(vpts.GetNumberOfPoints()): 2548 intersection: MutableSequence[float] = [0, 0, 0] 2549 vpts.GetPoint(i, intersection) 2550 pts.append(intersection) 2551 pts2 = np.array(pts) 2552 2553 if return_ids: 2554 pts_ids = [] 2555 for i in range(idlist.GetNumberOfIds()): 2556 cid = idlist.GetId(i) 2557 pts_ids.append(cid) 2558 return (pts2, np.array(pts_ids).astype(np.uint32)) 2559 2560 return pts2 2561 2562 def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 2563 """ 2564 Intersect this Mesh with a plane to return a set of lines. 2565 2566 Example: 2567 ```python 2568 from vedo import * 2569 sph = Sphere() 2570 mi = sph.clone().intersect_with_plane().join() 2571 print(mi.lines) 2572 show(sph, mi, axes=1).close() 2573 ``` 2574 ![](https://vedo.embl.es/images/feats/intersect_plane.png) 2575 """ 2576 plane = vtki.new("Plane") 2577 plane.SetOrigin(origin) 2578 plane.SetNormal(normal) 2579 2580 cutter = vtki.new("PolyDataPlaneCutter") 2581 cutter.SetInputData(self.dataset) 2582 cutter.SetPlane(plane) 2583 cutter.InterpolateAttributesOn() 2584 cutter.ComputeNormalsOff() 2585 cutter.Update() 2586 2587 msh = Mesh(cutter.GetOutput()) 2588 msh.c('k').lw(3).lighting("off") 2589 msh.pipeline = OperationNode( 2590 "intersect_with_plan", 2591 parents=[self], 2592 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2593 ) 2594 msh.name = "PlaneIntersection" 2595 return msh 2596 2597 def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]: 2598 """ 2599 Cut/clip a closed surface mesh with a collection of planes. 2600 This will produce a new closed surface by creating new polygonal 2601 faces where the input surface hits the planes. 2602 2603 The orientation of the polygons that form the surface is important. 2604 Polygons have a front face and a back face, and it's the back face that defines 2605 the interior or "solid" region of the closed surface. 2606 When a plane cuts through a "solid" region, a new cut face is generated, 2607 but not when a clipping plane cuts through a hole or "empty" region. 2608 This distinction is crucial when dealing with complex surfaces. 2609 Note that if a simple surface has its back faces pointing outwards, 2610 then that surface defines a hole in a potentially infinite solid. 2611 2612 Non-manifold surfaces should not be used with this method. 2613 2614 Arguments: 2615 origins : (list) 2616 list of plane origins 2617 normals : (list) 2618 list of plane normals 2619 invert : (bool) 2620 invert the clipping. 2621 return_assembly : (bool) 2622 return the cap and the clipped surfaces as a `vedo.Assembly`. 2623 2624 Example: 2625 ```python 2626 from vedo import * 2627 s = Sphere(res=50).linewidth(1) 2628 origins = [[-0.7, 0, 0], [0, -0.6, 0]] 2629 normals = [[-1, 0, 0], [0, -1, 0]] 2630 s.cut_closed_surface(origins, normals) 2631 show(s, axes=1).close() 2632 ``` 2633 """ 2634 planes = vtki.new("PlaneCollection") 2635 for p, s in zip(origins, normals): 2636 plane = vtki.vtkPlane() 2637 plane.SetOrigin(vedo.utils.make3d(p)) 2638 plane.SetNormal(vedo.utils.make3d(s)) 2639 planes.AddItem(plane) 2640 clipper = vtki.new("ClipClosedSurface") 2641 clipper.SetInputData(self.dataset) 2642 clipper.SetClippingPlanes(planes) 2643 clipper.PassPointDataOn() 2644 clipper.GenerateFacesOn() 2645 clipper.SetScalarModeToLabels() 2646 clipper.TriangulationErrorDisplayOn() 2647 clipper.SetInsideOut(not invert) 2648 2649 if return_assembly: 2650 clipper.GenerateClipFaceOutputOn() 2651 clipper.Update() 2652 parts = [] 2653 for i in range(clipper.GetNumberOfOutputPorts()): 2654 msh = Mesh(clipper.GetOutput(i)) 2655 msh.copy_properties_from(self) 2656 msh.name = "CutClosedSurface" 2657 msh.pipeline = OperationNode( 2658 "cut_closed_surface", 2659 parents=[self], 2660 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2661 ) 2662 parts.append(msh) 2663 asse = vedo.Assembly(parts) 2664 asse.name = "CutClosedSurface" 2665 return asse 2666 2667 else: 2668 clipper.GenerateClipFaceOutputOff() 2669 clipper.Update() 2670 self._update(clipper.GetOutput()) 2671 self.flat() 2672 self.name = "CutClosedSurface" 2673 self.pipeline = OperationNode( 2674 "cut_closed_surface", 2675 parents=[self], 2676 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 2677 ) 2678 return self 2679 2680 def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]: 2681 """ 2682 Collide this Mesh with the input surface. 2683 Information is stored in `ContactCells1` and `ContactCells2`. 2684 """ 2685 ipdf = vtki.new("CollisionDetectionFilter") 2686 # ipdf.SetGlobalWarningDisplay(0) 2687 2688 transform0 = vtki.vtkTransform() 2689 transform1 = vtki.vtkTransform() 2690 2691 # ipdf.SetBoxTolerance(tol) 2692 ipdf.SetCellTolerance(tol) 2693 ipdf.SetInputData(0, self.dataset) 2694 ipdf.SetInputData(1, mesh2.dataset) 2695 ipdf.SetTransform(0, transform0) 2696 ipdf.SetTransform(1, transform1) 2697 if return_bool: 2698 ipdf.SetCollisionModeToFirstContact() 2699 else: 2700 ipdf.SetCollisionModeToAllContacts() 2701 ipdf.Update() 2702 2703 if return_bool: 2704 return bool(ipdf.GetNumberOfContacts()) 2705 2706 msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off") 2707 msh.metadata["ContactCells1"] = vtk2numpy( 2708 ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells") 2709 ) 2710 msh.metadata["ContactCells2"] = vtk2numpy( 2711 ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells") 2712 ) 2713 msh.properties.SetLineWidth(3) 2714 2715 msh.pipeline = OperationNode( 2716 "collide_with", 2717 parents=[self, mesh2], 2718 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2719 ) 2720 msh.name = "SurfaceCollision" 2721 return msh 2722 2723 def geodesic(self, start, end) -> Self: 2724 """ 2725 Dijkstra algorithm to compute the geodesic line. 2726 Takes as input a polygonal mesh and performs a single source shortest path calculation. 2727 2728 The output mesh contains the array "VertexIDs" that contains the ordered list of vertices 2729 traversed to get from the start vertex to the end vertex. 2730 2731 Arguments: 2732 start : (int, list) 2733 start vertex index or close point `[x,y,z]` 2734 end : (int, list) 2735 end vertex index or close point `[x,y,z]` 2736 2737 Examples: 2738 - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py) 2739 2740 ![](https://vedo.embl.es/images/advanced/geodesic.png) 2741 """ 2742 if is_sequence(start): 2743 cc = self.vertices 2744 pa = Points(cc) 2745 start = pa.closest_point(start, return_point_id=True) 2746 end = pa.closest_point(end, return_point_id=True) 2747 2748 dijkstra = vtki.new("DijkstraGraphGeodesicPath") 2749 dijkstra.SetInputData(self.dataset) 2750 dijkstra.SetStartVertex(end) # inverted in vtk 2751 dijkstra.SetEndVertex(start) 2752 dijkstra.Update() 2753 2754 weights = vtki.vtkDoubleArray() 2755 dijkstra.GetCumulativeWeights(weights) 2756 2757 idlist = dijkstra.GetIdList() 2758 ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())] 2759 2760 length = weights.GetMaxId() + 1 2761 arr = np.zeros(length) 2762 for i in range(length): 2763 arr[i] = weights.GetTuple(i)[0] 2764 2765 poly = dijkstra.GetOutput() 2766 2767 vdata = numpy2vtk(arr) 2768 vdata.SetName("CumulativeWeights") 2769 poly.GetPointData().AddArray(vdata) 2770 2771 vdata2 = numpy2vtk(ids, dtype=np.uint) 2772 vdata2.SetName("VertexIDs") 2773 poly.GetPointData().AddArray(vdata2) 2774 poly.GetPointData().Modified() 2775 2776 dmesh = Mesh(poly).copy_properties_from(self) 2777 dmesh.lw(3).alpha(1).lighting("off") 2778 dmesh.name = "GeodesicLine" 2779 2780 dmesh.pipeline = OperationNode( 2781 "GeodesicLine", 2782 parents=[self], 2783 comment=f"#steps {poly.GetNumberOfPoints()}", 2784 ) 2785 return dmesh 2786 2787 ##################################################################### 2788 ### Stuff returning a Volume object 2789 ##################################################################### 2790 def binarize( 2791 self, 2792 values=(255, 0), 2793 spacing=None, 2794 dims=None, 2795 origin=None, 2796 ) -> "vedo.Volume": 2797 """ 2798 Convert a `Mesh` into a `Volume` where 2799 the interior voxels value is set to `values[0]` (255 by default), while 2800 the exterior voxels value is set to `values[1]` (0 by default). 2801 2802 Arguments: 2803 values : (list) 2804 background and foreground values. 2805 spacing : (list) 2806 voxel spacing in x, y and z. 2807 dims : (list) 2808 dimensions (nr. of voxels) of the output volume. 2809 origin : (list) 2810 position in space of the (0,0,0) voxel. 2811 2812 Examples: 2813 - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py) 2814 2815 ![](https://vedo.embl.es/images/volumetric/mesh2volume.png) 2816 """ 2817 assert len(values) == 2, "values must be a list of 2 values" 2818 fg_value, bg_value = values 2819 2820 bounds = self.bounds() 2821 if spacing is None: # compute spacing 2822 spacing = [0, 0, 0] 2823 diagonal = np.sqrt( 2824 (bounds[1] - bounds[0]) ** 2 2825 + (bounds[3] - bounds[2]) ** 2 2826 + (bounds[5] - bounds[4]) ** 2 2827 ) 2828 spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0 2829 2830 if dims is None: # compute dimensions 2831 dim = [0, 0, 0] 2832 for i in [0, 1, 2]: 2833 dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i])) 2834 else: 2835 dim = dims 2836 2837 white_img = vtki.vtkImageData() 2838 white_img.SetDimensions(dim) 2839 white_img.SetSpacing(spacing) 2840 white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1) 2841 2842 if origin is None: 2843 origin = [0, 0, 0] 2844 origin[0] = bounds[0] + spacing[0] 2845 origin[1] = bounds[2] + spacing[1] 2846 origin[2] = bounds[4] + spacing[2] 2847 white_img.SetOrigin(origin) 2848 2849 # if direction_matrix is not None: 2850 # white_img.SetDirectionMatrix(direction_matrix) 2851 2852 white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 2853 2854 # fill the image with foreground voxels: 2855 white_img.GetPointData().GetScalars().Fill(fg_value) 2856 2857 # polygonal data --> image stencil: 2858 pol2stenc = vtki.new("PolyDataToImageStencil") 2859 pol2stenc.SetInputData(self.dataset) 2860 pol2stenc.SetOutputOrigin(white_img.GetOrigin()) 2861 pol2stenc.SetOutputSpacing(white_img.GetSpacing()) 2862 pol2stenc.SetOutputWholeExtent(white_img.GetExtent()) 2863 pol2stenc.Update() 2864 2865 # cut the corresponding white image and set the background: 2866 imgstenc = vtki.new("ImageStencil") 2867 imgstenc.SetInputData(white_img) 2868 imgstenc.SetStencilConnection(pol2stenc.GetOutputPort()) 2869 # imgstenc.SetReverseStencil(True) 2870 imgstenc.SetBackgroundValue(bg_value) 2871 imgstenc.Update() 2872 2873 vol = vedo.Volume(imgstenc.GetOutput()) 2874 vol.name = "BinarizedVolume" 2875 vol.pipeline = OperationNode( 2876 "binarize", 2877 parents=[self], 2878 comment=f"dims={tuple(vol.dimensions())}", 2879 c="#e9c46a:#0096c7", 2880 ) 2881 return vol 2882 2883 def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume": 2884 """ 2885 Compute the `Volume` object whose voxels contains 2886 the signed distance from the mesh. 2887 2888 Arguments: 2889 bounds : (list) 2890 bounds of the output volume 2891 dims : (list) 2892 dimensions (nr. of voxels) of the output volume 2893 invert : (bool) 2894 flip the sign 2895 2896 Examples: 2897 - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py) 2898 """ 2899 if maxradius is not None: 2900 vedo.logger.warning( 2901 "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)." 2902 ) 2903 if bounds is None: 2904 bounds = self.bounds() 2905 sx = (bounds[1] - bounds[0]) / dims[0] 2906 sy = (bounds[3] - bounds[2]) / dims[1] 2907 sz = (bounds[5] - bounds[4]) / dims[2] 2908 2909 img = vtki.vtkImageData() 2910 img.SetDimensions(dims) 2911 img.SetSpacing(sx, sy, sz) 2912 img.SetOrigin(bounds[0], bounds[2], bounds[4]) 2913 img.AllocateScalars(vtki.VTK_FLOAT, 1) 2914 2915 imp = vtki.new("ImplicitPolyDataDistance") 2916 imp.SetInput(self.dataset) 2917 b2 = bounds[2] 2918 b4 = bounds[4] 2919 d0, d1, d2 = dims 2920 2921 for i in range(d0): 2922 x = i * sx + bounds[0] 2923 for j in range(d1): 2924 y = j * sy + b2 2925 for k in range(d2): 2926 v = imp.EvaluateFunction((x, y, k * sz + b4)) 2927 if invert: 2928 v = -v 2929 img.SetScalarComponentFromFloat(i, j, k, 0, v) 2930 2931 vol = vedo.Volume(img) 2932 vol.name = "SignedVolume" 2933 2934 vol.pipeline = OperationNode( 2935 "signed_distance", 2936 parents=[self], 2937 comment=f"dims={tuple(vol.dimensions())}", 2938 c="#e9c46a:#0096c7", 2939 ) 2940 return vol 2941 2942 def tetralize( 2943 self, 2944 side=0.02, 2945 nmax=300_000, 2946 gap=None, 2947 subsample=False, 2948 uniform=True, 2949 seed=0, 2950 debug=False, 2951 ) -> "vedo.TetMesh": 2952 """ 2953 Tetralize a closed polygonal mesh. Return a `TetMesh`. 2954 2955 Arguments: 2956 side : (float) 2957 desired side of the single tetras as fraction of the bounding box diagonal. 2958 Typical values are in the range (0.01 - 0.03) 2959 nmax : (int) 2960 maximum random numbers to be sampled in the bounding box 2961 gap : (float) 2962 keep this minimum distance from the surface, 2963 if None an automatic choice is made. 2964 subsample : (bool) 2965 subsample input surface, the geometry might be affected 2966 (the number of original faces reduceed), but higher tet quality might be obtained. 2967 uniform : (bool) 2968 generate tets more uniformly packed in the interior of the mesh 2969 seed : (int) 2970 random number generator seed 2971 debug : (bool) 2972 show an intermediate plot with sampled points 2973 2974 Examples: 2975 - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py) 2976 2977 ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg) 2978 """ 2979 surf = self.clone().clean().compute_normals() 2980 d = surf.diagonal_size() 2981 if gap is None: 2982 gap = side * d * np.sqrt(2 / 3) 2983 n = int(min((1 / side) ** 3, nmax)) 2984 2985 # fill the space w/ points 2986 x0, x1, y0, y1, z0, z1 = surf.bounds() 2987 2988 if uniform: 2989 pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42) 2990 pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100 # some small jitter 2991 else: 2992 disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2 2993 np.random.seed(seed) 2994 pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp 2995 2996 normals = surf.celldata["Normals"] 2997 cc = surf.cell_centers 2998 subpts = cc - normals * gap * 1.05 2999 pts = pts.tolist() + subpts.tolist() 3000 3001 if debug: 3002 print(".. tetralize(): subsampling and cleaning") 3003 3004 fillpts = surf.inside_points(pts) 3005 fillpts.subsample(side) 3006 3007 if gap: 3008 fillpts.distance_to(surf) 3009 fillpts.threshold("Distance", above=gap) 3010 3011 if subsample: 3012 surf.subsample(side) 3013 3014 merged_fs = vedo.merge(fillpts, surf) 3015 tmesh = merged_fs.generate_delaunay3d() 3016 tcenters = tmesh.cell_centers 3017 3018 ids = surf.inside_points(tcenters, return_ids=True) 3019 ins = np.zeros(tmesh.ncells) 3020 ins[ids] = 1 3021 3022 if debug: 3023 # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close() 3024 edges = self.edges 3025 points = self.vertices 3026 elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :]) 3027 histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d)) 3028 print(".. edges min, max", elen.min(), elen.max()) 3029 fillpts.cmap("bone") 3030 vedo.show( 3031 [ 3032 [ 3033 f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}", 3034 surf.wireframe().alpha(0.2), 3035 vedo.addons.Axes(surf), 3036 fillpts, 3037 Points(subpts).c("r4").ps(3), 3038 ], 3039 [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo], 3040 ], 3041 N=2, 3042 sharecam=False, 3043 new=True, 3044 ).close() 3045 print(".. thresholding") 3046 3047 tmesh.celldata["inside"] = ins.astype(np.uint8) 3048 tmesh.threshold("inside", above=0.9) 3049 tmesh.celldata.remove("inside") 3050 3051 if debug: 3052 print(f".. tetralize() completed, ntets = {tmesh.ncells}") 3053 3054 tmesh.pipeline = OperationNode( 3055 "tetralize", 3056 parents=[self], 3057 comment=f"#tets = {tmesh.ncells}", 3058 c="#e9c46a:#9e2a2b", 3059 ) 3060 return tmesh
29class Mesh(MeshVisual, Points): 30 """ 31 Build an instance of object `Mesh` derived from `vedo.PointCloud`. 32 """ 33 34 def __init__(self, inputobj=None, c="gold", alpha=1): 35 """ 36 Initialize a ``Mesh`` object. 37 38 Arguments: 39 inputobj : (str, vtkPolyData, vtkActor, vedo.Mesh) 40 If inputobj is `None` an empty mesh is created. 41 If inputobj is a `str` then it is interpreted as the name of a file to load as mesh. 42 If inputobj is an `vtkPolyData` or `vtkActor` or `vedo.Mesh` 43 then a shallow copy of it is created. 44 If inputobj is a `vedo.Mesh` then a shallow copy of it is created. 45 46 Examples: 47 - [buildmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buildmesh.py) 48 (and many others!) 49 50 ![](https://vedo.embl.es/images/basic/buildmesh.png) 51 """ 52 # print("INIT MESH", super()) 53 super().__init__() 54 55 self.name = "Mesh" 56 57 if inputobj is None: 58 # self.dataset = vtki.vtkPolyData() 59 pass 60 61 elif isinstance(inputobj, str): 62 self.dataset = vedo.file_io.load(inputobj).dataset 63 self.filename = inputobj 64 65 elif isinstance(inputobj, vtki.vtkPolyData): 66 # self.dataset.DeepCopy(inputobj) # NO 67 self.dataset = inputobj 68 if self.dataset.GetNumberOfCells() == 0: 69 carr = vtki.vtkCellArray() 70 for i in range(inputobj.GetNumberOfPoints()): 71 carr.InsertNextCell(1) 72 carr.InsertCellPoint(i) 73 self.dataset.SetVerts(carr) 74 75 elif isinstance(inputobj, Mesh): 76 self.dataset = inputobj.dataset 77 78 elif is_sequence(inputobj): 79 ninp = len(inputobj) 80 if ninp == 4: # assume input is [vertices, faces, lines, strips] 81 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2], inputobj[3]) 82 elif ninp == 3: # assume input is [vertices, faces, lines] 83 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2]) 84 elif ninp == 2: # assume input is [vertices, faces] 85 self.dataset = buildPolyData(inputobj[0], inputobj[1]) 86 elif ninp == 1: # assume input is [vertices] 87 self.dataset = buildPolyData(inputobj[0]) 88 else: 89 vedo.logger.error("input must be a list of max 4 elements.") 90 raise ValueError() 91 92 elif isinstance(inputobj, vtki.vtkActor): 93 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 94 v = inputobj.GetMapper().GetScalarVisibility() 95 self.mapper.SetScalarVisibility(v) 96 pr = vtki.vtkProperty() 97 pr.DeepCopy(inputobj.GetProperty()) 98 self.actor.SetProperty(pr) 99 self.properties = pr 100 101 elif isinstance(inputobj, (vtki.vtkStructuredGrid, vtki.vtkRectilinearGrid)): 102 gf = vtki.new("GeometryFilter") 103 gf.SetInputData(inputobj) 104 gf.Update() 105 self.dataset = gf.GetOutput() 106 107 elif "meshlab" in str(type(inputobj)): 108 self.dataset = vedo.utils.meshlab2vedo(inputobj).dataset 109 110 elif "meshlib" in str(type(inputobj)): 111 import meshlib.mrmeshnumpy as mrmeshnumpy 112 self.dataset = buildPolyData( 113 mrmeshnumpy.getNumpyVerts(inputobj), 114 mrmeshnumpy.getNumpyFaces(inputobj.topology), 115 ) 116 117 elif "trimesh" in str(type(inputobj)): 118 self.dataset = vedo.utils.trimesh2vedo(inputobj).dataset 119 120 elif "meshio" in str(type(inputobj)): 121 # self.dataset = vedo.utils.meshio2vedo(inputobj) ##TODO 122 if len(inputobj.cells) > 0: 123 mcells = [] 124 for cellblock in inputobj.cells: 125 if cellblock.type in ("triangle", "quad"): 126 mcells += cellblock.data.tolist() 127 self.dataset = buildPolyData(inputobj.points, mcells) 128 else: 129 self.dataset = buildPolyData(inputobj.points, None) 130 # add arrays: 131 try: 132 if len(inputobj.point_data) > 0: 133 for k in inputobj.point_data.keys(): 134 vdata = numpy2vtk(inputobj.point_data[k]) 135 vdata.SetName(str(k)) 136 self.dataset.GetPointData().AddArray(vdata) 137 except AssertionError: 138 print("Could not add meshio point data, skip.") 139 140 else: 141 try: 142 gf = vtki.new("GeometryFilter") 143 gf.SetInputData(inputobj) 144 gf.Update() 145 self.dataset = gf.GetOutput() 146 except: 147 vedo.logger.error(f"cannot build mesh from type {type(inputobj)}") 148 raise RuntimeError() 149 150 self.mapper.SetInputData(self.dataset) 151 self.actor.SetMapper(self.mapper) 152 153 self.properties.SetInterpolationToPhong() 154 self.properties.SetColor(get_color(c)) 155 156 if alpha is not None: 157 self.properties.SetOpacity(alpha) 158 159 self.mapper.SetInterpolateScalarsBeforeMapping( 160 vedo.settings.interpolate_scalars_before_mapping 161 ) 162 163 if vedo.settings.use_polygon_offset: 164 self.mapper.SetResolveCoincidentTopologyToPolygonOffset() 165 pof = vedo.settings.polygon_offset_factor 166 pou = vedo.settings.polygon_offset_units 167 self.mapper.SetResolveCoincidentTopologyPolygonOffsetParameters(pof, pou) 168 169 n = self.dataset.GetNumberOfPoints() 170 self.pipeline = OperationNode(self, comment=f"#pts {n}") 171 172 def _repr_html_(self): 173 """ 174 HTML representation of the Mesh object for Jupyter Notebooks. 175 176 Returns: 177 HTML text with the image and some properties. 178 """ 179 import io 180 import base64 181 from PIL import Image 182 183 library_name = "vedo.mesh.Mesh" 184 help_url = "https://vedo.embl.es/docs/vedo/mesh.html#Mesh" 185 186 arr = self.thumbnail() 187 im = Image.fromarray(arr) 188 buffered = io.BytesIO() 189 im.save(buffered, format="PNG", quality=100) 190 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 191 url = "data:image/png;base64," + encoded 192 image = f"<img src='{url}'></img>" 193 194 bounds = "<br/>".join( 195 [ 196 precision(min_x, 4) + " ... " + precision(max_x, 4) 197 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 198 ] 199 ) 200 average_size = "{size:.3f}".format(size=self.average_size()) 201 202 help_text = "" 203 if self.name: 204 help_text += f"<b> {self.name}:   </b>" 205 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 206 if self.filename: 207 dots = "" 208 if len(self.filename) > 30: 209 dots = "..." 210 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 211 212 pdata = "" 213 if self.dataset.GetPointData().GetScalars(): 214 if self.dataset.GetPointData().GetScalars().GetName(): 215 name = self.dataset.GetPointData().GetScalars().GetName() 216 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 217 218 cdata = "" 219 if self.dataset.GetCellData().GetScalars(): 220 if self.dataset.GetCellData().GetScalars().GetName(): 221 name = self.dataset.GetCellData().GetScalars().GetName() 222 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 223 224 allt = [ 225 "<table>", 226 "<tr>", 227 "<td>", 228 image, 229 "</td>", 230 "<td style='text-align: center; vertical-align: center;'><br/>", 231 help_text, 232 "<table>", 233 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 234 "<tr><td><b> center of mass </b></td><td>" 235 + precision(self.center_of_mass(), 3) 236 + "</td></tr>", 237 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 238 "<tr><td><b> nr. points / faces </b></td><td>" 239 + str(self.npoints) 240 + " / " 241 + str(self.ncells) 242 + "</td></tr>", 243 pdata, 244 cdata, 245 "</table>", 246 "</table>", 247 ] 248 return "\n".join(allt) 249 250 def faces(self, ids=()): 251 """DEPRECATED. Use property `mesh.cells` instead.""" 252 vedo.printc("WARNING: use property mesh.cells instead of mesh.faces()",c='y') 253 return self.cells 254 255 @property 256 def edges(self): 257 """Return an array containing the edges connectivity.""" 258 extractEdges = vtki.new("ExtractEdges") 259 extractEdges.SetInputData(self.dataset) 260 # eed.UseAllPointsOn() 261 extractEdges.Update() 262 lpoly = extractEdges.GetOutput() 263 264 arr1d = vtk2numpy(lpoly.GetLines().GetData()) 265 # [nids1, id0 ... idn, niids2, id0 ... idm, etc]. 266 267 i = 0 268 conn = [] 269 n = len(arr1d) 270 for _ in range(n): 271 cell = [arr1d[i + k + 1] for k in range(arr1d[i])] 272 conn.append(cell) 273 i += arr1d[i] + 1 274 if i >= n: 275 break 276 return conn # cannot always make a numpy array of it! 277 278 @property 279 def cell_normals(self): 280 """ 281 Retrieve face normals as a numpy array. 282 Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`. 283 """ 284 vtknormals = self.dataset.GetCellData().GetNormals() 285 numpy_normals = vtk2numpy(vtknormals) 286 if len(numpy_normals) == 0 and len(self.cells) != 0: 287 vedo.logger.warning( 288 "failed to return normal vectors.\n" 289 "You may need to call `Mesh.compute_normals()` before accessing 'Mesh.cell_normals'." 290 ) 291 numpy_normals = np.zeros((self.ncells, 3)) + [0,0,1] 292 return numpy_normals 293 294 def compute_normals(self, points=True, cells=True, feature_angle=None, consistency=True) -> Self: 295 """ 296 Compute cell and vertex normals for the mesh. 297 298 Arguments: 299 points : (bool) 300 do the computation for the vertices too 301 cells : (bool) 302 do the computation for the cells too 303 feature_angle : (float) 304 specify the angle that defines a sharp edge. 305 If the difference in angle across neighboring polygons is greater than this value, 306 the shared edge is considered "sharp" and it is split. 307 consistency : (bool) 308 turn on/off the enforcement of consistent polygon ordering. 309 310 .. warning:: 311 If `feature_angle` is set then the Mesh can be modified, and it 312 can have a different nr. of vertices from the original. 313 314 Note that the appearance of the mesh may change if the normals are computed, 315 as shading is automatically enabled when such information is present. 316 Use `mesh.flat()` to avoid smoothing effects. 317 """ 318 pdnorm = vtki.new("PolyDataNormals") 319 pdnorm.SetInputData(self.dataset) 320 pdnorm.SetComputePointNormals(points) 321 pdnorm.SetComputeCellNormals(cells) 322 pdnorm.SetConsistency(consistency) 323 pdnorm.FlipNormalsOff() 324 if feature_angle: 325 pdnorm.SetSplitting(True) 326 pdnorm.SetFeatureAngle(feature_angle) 327 else: 328 pdnorm.SetSplitting(False) 329 pdnorm.Update() 330 out = pdnorm.GetOutput() 331 self._update(out, reset_locators=False) 332 return self 333 334 def reverse(self, cells=True, normals=False) -> Self: 335 """ 336 Reverse the order of polygonal cells 337 and/or reverse the direction of point and cell normals. 338 339 Two flags are used to control these operations: 340 - `cells=True` reverses the order of the indices in the cell connectivity list. 341 If cell is a list of IDs only those cells will be reversed. 342 - `normals=True` reverses the normals by multiplying the normal vector by -1 343 (both point and cell normals, if present). 344 """ 345 poly = self.dataset 346 347 if is_sequence(cells): 348 for cell in cells: 349 poly.ReverseCell(cell) 350 poly.GetCellData().Modified() 351 return self ############## 352 353 rev = vtki.new("ReverseSense") 354 if cells: 355 rev.ReverseCellsOn() 356 else: 357 rev.ReverseCellsOff() 358 if normals: 359 rev.ReverseNormalsOn() 360 else: 361 rev.ReverseNormalsOff() 362 rev.SetInputData(poly) 363 rev.Update() 364 self._update(rev.GetOutput(), reset_locators=False) 365 self.pipeline = OperationNode("reverse", parents=[self]) 366 return self 367 368 def volume(self) -> float: 369 """ 370 Compute the volume occupied by mesh. 371 The mesh must be triangular for this to work. 372 To triangulate a mesh use `mesh.triangulate()`. 373 """ 374 mass = vtki.new("MassProperties") 375 mass.SetGlobalWarningDisplay(0) 376 mass.SetInputData(self.dataset) 377 mass.Update() 378 mass.SetGlobalWarningDisplay(1) 379 return mass.GetVolume() 380 381 def area(self) -> float: 382 """ 383 Compute the surface area of the mesh. 384 The mesh must be triangular for this to work. 385 To triangulate a mesh use `mesh.triangulate()`. 386 """ 387 mass = vtki.new("MassProperties") 388 mass.SetGlobalWarningDisplay(0) 389 mass.SetInputData(self.dataset) 390 mass.Update() 391 mass.SetGlobalWarningDisplay(1) 392 return mass.GetSurfaceArea() 393 394 def is_closed(self) -> bool: 395 """ 396 Return `True` if the mesh is watertight. 397 Note that if the mesh contains coincident points the result may be flase. 398 Use in this case `mesh.clean()` to merge coincident points. 399 """ 400 fe = vtki.new("FeatureEdges") 401 fe.BoundaryEdgesOn() 402 fe.FeatureEdgesOff() 403 fe.NonManifoldEdgesOn() 404 fe.SetInputData(self.dataset) 405 fe.Update() 406 ne = fe.GetOutput().GetNumberOfCells() 407 return not bool(ne) 408 409 def is_manifold(self) -> bool: 410 """Return `True` if the mesh is manifold.""" 411 fe = vtki.new("FeatureEdges") 412 fe.BoundaryEdgesOff() 413 fe.FeatureEdgesOff() 414 fe.NonManifoldEdgesOn() 415 fe.SetInputData(self.dataset) 416 fe.Update() 417 ne = fe.GetOutput().GetNumberOfCells() 418 return not bool(ne) 419 420 def non_manifold_faces(self, remove=True, tol="auto") -> Self: 421 """ 422 Detect and (try to) remove non-manifold faces of a triangular mesh: 423 424 - set `remove` to `False` to mark cells without removing them. 425 - set `tol=0` for zero-tolerance, the result will be manifold but with holes. 426 - set `tol>0` to cut off non-manifold faces, and try to recover the good ones. 427 - set `tol="auto"` to make an automatic choice of the tolerance. 428 """ 429 # mark original point and cell ids 430 self.add_ids() 431 toremove = self.boundaries( 432 boundary_edges=False, 433 non_manifold_edges=True, 434 cell_edge=True, 435 return_cell_ids=True, 436 ) 437 if len(toremove) == 0: # type: ignore 438 return self 439 440 points = self.vertices 441 faces = self.cells 442 centers = self.cell_centers 443 444 copy = self.clone() 445 copy.delete_cells(toremove).clean() 446 copy.compute_normals(cells=False) 447 normals = copy.vertex_normals 448 deltas, deltas_i = [], [] 449 450 for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"): 451 pids = copy.closest_point(centers[i], n=3, return_point_id=True) 452 norms = normals[pids] 453 n = np.mean(norms, axis=0) 454 dn = np.linalg.norm(n) 455 if not dn: 456 continue 457 n = n / dn 458 459 p0, p1, p2 = points[faces[i]][:3] 460 v = np.cross(p1 - p0, p2 - p0) 461 lv = np.linalg.norm(v) 462 if not lv: 463 continue 464 v = v / lv 465 466 cosa = 1 - np.dot(n, v) 467 deltas.append(cosa) 468 deltas_i.append(i) 469 470 recover = [] 471 if len(deltas) > 0: 472 mean_delta = np.mean(deltas) 473 err_delta = np.std(deltas) 474 txt = "" 475 if tol == "auto": # automatic choice 476 tol = mean_delta / 5 477 txt = f"\n Automatic tol. : {tol: .4f}" 478 for i, cosa in zip(deltas_i, deltas): 479 if cosa < tol: 480 recover.append(i) 481 482 vedo.logger.info( 483 f"\n --------- Non manifold faces ---------" 484 f"\n Average tol. : {mean_delta: .4f} +- {err_delta: .4f}{txt}" 485 f"\n Removed faces : {len(toremove)}" # type: ignore 486 f"\n Recovered faces: {len(recover)}" 487 ) 488 489 toremove = list(set(toremove) - set(recover)) # type: ignore 490 491 if not remove: 492 mark = np.zeros(self.ncells, dtype=np.uint8) 493 mark[recover] = 1 494 mark[toremove] = 2 495 self.celldata["NonManifoldCell"] = mark 496 else: 497 self.delete_cells(toremove) # type: ignore 498 499 self.pipeline = OperationNode( 500 "non_manifold_faces", 501 parents=[self], 502 comment=f"#cells {self.dataset.GetNumberOfCells()}", 503 ) 504 return self 505 506 507 def euler_characteristic(self) -> int: 508 """ 509 Compute the Euler characteristic of the mesh. 510 The Euler characteristic is a topological invariant for surfaces. 511 """ 512 return self.npoints - len(self.edges) + self.ncells 513 514 def genus(self) -> int: 515 """ 516 Compute the genus of the mesh. 517 The genus is a topological invariant for surfaces. 518 """ 519 nb = len(self.boundaries().split()) - 1 520 return (2 - self.euler_characteristic() - nb ) / 2 521 522 def to_reeb_graph(self, field_id=0): 523 """ 524 Convert the mesh into a Reeb graph. 525 The Reeb graph is a topological structure that captures the evolution 526 of the level sets of a scalar field. 527 528 Arguments: 529 field_id : (int) 530 the id of the scalar field to use. 531 532 Example: 533 ```python 534 from vedo import * 535 mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl") 536 mesh.rotate_x(10).rotate_y(15).alpha(0.5) 537 mesh.pointdata["scalars"] = mesh.vertices[:, 2] 538 539 printc("is_closed :", mesh.is_closed()) 540 printc("is_manifold:", mesh.is_manifold()) 541 printc("euler_char :", mesh.euler_characteristic()) 542 printc("genus :", mesh.genus()) 543 544 reeb = mesh.to_reeb_graph() 545 ids = reeb[0].pointdata["Vertex Ids"] 546 pts = Points(mesh.vertices[ids], r=10) 547 548 show([[mesh, pts], reeb], N=2, sharecam=False) 549 ``` 550 """ 551 rg = vtki.new("PolyDataToReebGraphFilter") 552 rg.SetInputData(self.dataset) 553 rg.SetFieldId(field_id) 554 rg.Update() 555 gr = vedo.pyplot.DirectedGraph() 556 gr.mdg = rg.GetOutput() 557 gr.build() 558 return gr 559 560 561 def shrink(self, fraction=0.85) -> Self: 562 """ 563 Shrink the triangle polydata in the representation of the input mesh. 564 565 Examples: 566 - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py) 567 568 ![](https://vedo.embl.es/images/basic/shrink.png) 569 """ 570 # Overriding base class method core.shrink() 571 shrink = vtki.new("ShrinkPolyData") 572 shrink.SetInputData(self.dataset) 573 shrink.SetShrinkFactor(fraction) 574 shrink.Update() 575 self._update(shrink.GetOutput()) 576 self.pipeline = OperationNode("shrink", parents=[self]) 577 return self 578 579 def cap(self, return_cap=False) -> Self: 580 """ 581 Generate a "cap" on a clipped mesh, or caps sharp edges. 582 583 Examples: 584 - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py) 585 586 ![](https://vedo.embl.es/images/advanced/cutAndCap.png) 587 588 See also: `join()`, `join_segments()`, `slice()`. 589 """ 590 fe = vtki.new("FeatureEdges") 591 fe.SetInputData(self.dataset) 592 fe.BoundaryEdgesOn() 593 fe.FeatureEdgesOff() 594 fe.NonManifoldEdgesOff() 595 fe.ManifoldEdgesOff() 596 fe.Update() 597 598 stripper = vtki.new("Stripper") 599 stripper.SetInputData(fe.GetOutput()) 600 stripper.JoinContiguousSegmentsOn() 601 stripper.Update() 602 603 boundary_poly = vtki.vtkPolyData() 604 boundary_poly.SetPoints(stripper.GetOutput().GetPoints()) 605 boundary_poly.SetPolys(stripper.GetOutput().GetLines()) 606 607 rev = vtki.new("ReverseSense") 608 rev.ReverseCellsOn() 609 rev.SetInputData(boundary_poly) 610 rev.Update() 611 612 tf = vtki.new("TriangleFilter") 613 tf.SetInputData(rev.GetOutput()) 614 tf.Update() 615 616 if return_cap: 617 m = Mesh(tf.GetOutput()) 618 m.pipeline = OperationNode( 619 "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}" 620 ) 621 m.name = "MeshCap" 622 return m 623 624 polyapp = vtki.new("AppendPolyData") 625 polyapp.AddInputData(self.dataset) 626 polyapp.AddInputData(tf.GetOutput()) 627 polyapp.Update() 628 629 self._update(polyapp.GetOutput()) 630 self.clean() 631 632 self.pipeline = OperationNode( 633 "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 634 ) 635 return self 636 637 def join(self, polys=True, reset=False) -> Self: 638 """ 639 Generate triangle strips and/or polylines from 640 input polygons, triangle strips, and lines. 641 642 Input polygons are assembled into triangle strips only if they are triangles; 643 other types of polygons are passed through to the output and not stripped. 644 Use mesh.triangulate() to triangulate non-triangular polygons prior to running 645 this filter if you need to strip all the data. 646 647 Also note that if triangle strips or polylines are present in the input 648 they are passed through and not joined nor extended. 649 If you wish to strip these use mesh.triangulate() to fragment the input 650 into triangles and lines prior to applying join(). 651 652 Arguments: 653 polys : (bool) 654 polygonal segments will be joined if they are contiguous 655 reset : (bool) 656 reset points ordering 657 658 Warning: 659 If triangle strips or polylines exist in the input data 660 they will be passed through to the output data. 661 This filter will only construct triangle strips if triangle polygons 662 are available; and will only construct polylines if lines are available. 663 664 Example: 665 ```python 666 from vedo import * 667 c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate() 668 c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate() 669 intersect = c1.intersect_with(c2).join(reset=True) 670 spline = Spline(intersect).c('blue').lw(5) 671 show(c1, c2, spline, intersect.labels('id'), axes=1).close() 672 ``` 673 ![](https://vedo.embl.es/images/feats/line_join.png) 674 """ 675 sf = vtki.new("Stripper") 676 sf.SetPassThroughCellIds(True) 677 sf.SetPassThroughPointIds(True) 678 sf.SetJoinContiguousSegments(polys) 679 sf.SetInputData(self.dataset) 680 sf.Update() 681 if reset: 682 poly = sf.GetOutput() 683 cpd = vtki.new("CleanPolyData") 684 cpd.PointMergingOn() 685 cpd.ConvertLinesToPointsOn() 686 cpd.ConvertPolysToLinesOn() 687 cpd.ConvertStripsToPolysOn() 688 cpd.SetInputData(poly) 689 cpd.Update() 690 poly = cpd.GetOutput() 691 vpts = poly.GetCell(0).GetPoints().GetData() 692 poly.GetPoints().SetData(vpts) 693 else: 694 poly = sf.GetOutput() 695 696 self._update(poly) 697 698 self.pipeline = OperationNode( 699 "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 700 ) 701 return self 702 703 def join_segments(self, closed=True, tol=1e-03) -> list: 704 """ 705 Join line segments into contiguous lines. 706 Useful to call with `triangulate()` method. 707 708 Returns: 709 list of `shapes.Lines` 710 711 Example: 712 ```python 713 from vedo import * 714 msh = Torus().alpha(0.1).wireframe() 715 intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5') 716 slices = [s.triangulate() for s in intersection.join_segments()] 717 show(msh, intersection, merge(slices), axes=1, viewup='z') 718 ``` 719 ![](https://vedo.embl.es/images/feats/join_segments.jpg) 720 """ 721 vlines = [] 722 for ipiece, outline in enumerate(self.split(must_share_edge=False)): # type: ignore 723 724 outline.clean() 725 pts = outline.vertices 726 if len(pts) < 3: 727 continue 728 avesize = outline.average_size() 729 lines = outline.lines 730 # print("---lines", lines, "in piece", ipiece) 731 tol = avesize / pts.shape[0] * tol 732 733 k = 0 734 joinedpts = [pts[k]] 735 for _ in range(len(pts)): 736 pk = pts[k] 737 for j, line in enumerate(lines): 738 739 id0, id1 = line[0], line[-1] 740 p0, p1 = pts[id0], pts[id1] 741 742 if np.linalg.norm(p0 - pk) < tol: 743 n = len(line) 744 for m in range(1, n): 745 joinedpts.append(pts[line[m]]) 746 # joinedpts.append(p1) 747 k = id1 748 lines.pop(j) 749 break 750 751 elif np.linalg.norm(p1 - pk) < tol: 752 n = len(line) 753 for m in reversed(range(0, n - 1)): 754 joinedpts.append(pts[line[m]]) 755 # joinedpts.append(p0) 756 k = id0 757 lines.pop(j) 758 break 759 760 if len(joinedpts) > 1: 761 newline = vedo.shapes.Line(joinedpts, closed=closed) 762 newline.clean() 763 newline.actor.SetProperty(self.properties) 764 newline.properties = self.properties 765 newline.pipeline = OperationNode( 766 "join_segments", 767 parents=[self], 768 comment=f"#pts {newline.dataset.GetNumberOfPoints()}", 769 ) 770 vlines.append(newline) 771 772 return vlines 773 774 def join_with_strips(self, b1, closed=True) -> Self: 775 """ 776 Join booundary lines by creating a triangle strip between them. 777 778 Example: 779 ```python 780 from vedo import * 781 m1 = Cylinder(cap=False).boundaries() 782 m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1) 783 strips = m1.join_with_strips(m2) 784 show(m1, m2, strips, axes=1).close() 785 ``` 786 """ 787 b0 = self.clone().join() 788 b1 = b1.clone().join() 789 790 vertices0 = b0.vertices.tolist() 791 vertices1 = b1.vertices.tolist() 792 793 lines0 = b0.lines 794 lines1 = b1.lines 795 m = len(lines0) 796 assert m == len(lines1), ( 797 "lines must have the same number of points\n" 798 f"line has {m} points in b0 and {len(lines1)} in b1" 799 ) 800 801 strips = [] 802 points: List[Any] = [] 803 804 for j in range(m): 805 806 ids0j = list(lines0[j]) 807 ids1j = list(lines1[j]) 808 809 n = len(ids0j) 810 assert n == len(ids1j), ( 811 "lines must have the same number of points\n" 812 f"line {j} has {n} points in b0 and {len(ids1j)} in b1" 813 ) 814 815 if closed: 816 ids0j.append(ids0j[0]) 817 ids1j.append(ids1j[0]) 818 vertices0.append(vertices0[ids0j[0]]) 819 vertices1.append(vertices1[ids1j[0]]) 820 n = n + 1 821 822 strip = [] # create a triangle strip 823 npt = len(points) 824 for ipt in range(n): 825 points.append(vertices0[ids0j[ipt]]) 826 points.append(vertices1[ids1j[ipt]]) 827 828 strip = list(range(npt, npt + 2*n)) 829 strips.append(strip) 830 831 return Mesh([points, [], [], strips], c="k6") 832 833 def split_polylines(self) -> Self: 834 """Split polylines into separate segments.""" 835 tf = vtki.new("TriangleFilter") 836 tf.SetPassLines(True) 837 tf.SetPassVerts(False) 838 tf.SetInputData(self.dataset) 839 tf.Update() 840 self._update(tf.GetOutput(), reset_locators=False) 841 self.lw(0).lighting("default").pickable() 842 self.pipeline = OperationNode( 843 "split_polylines", parents=[self], 844 comment=f"#lines {self.dataset.GetNumberOfLines()}" 845 ) 846 return self 847 848 def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 849 """ 850 Slice a mesh with a plane and fill the contour. 851 852 Example: 853 ```python 854 from vedo import * 855 msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe() 856 mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0]) 857 mslice.c('purple5') 858 show(msh, mslice, axes=1) 859 ``` 860 ![](https://vedo.embl.es/images/feats/mesh_slice.jpg) 861 862 See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`. 863 """ 864 intersection = self.intersect_with_plane(origin=origin, normal=normal) 865 slices = [s.triangulate() for s in intersection.join_segments()] 866 mslices = vedo.pointcloud.merge(slices) 867 if mslices: 868 mslices.name = "MeshSlice" 869 mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}") 870 return mslices 871 872 def triangulate(self, verts=True, lines=True) -> Self: 873 """ 874 Converts mesh polygons into triangles. 875 876 If the input mesh is only made of 2D lines (no faces) the output will be a triangulation 877 that fills the internal area. The contours may be concave, and may even contain holes, 878 i.e. a contour may contain an internal contour winding in the opposite 879 direction to indicate that it is a hole. 880 881 Arguments: 882 verts : (bool) 883 if True, break input vertex cells into individual vertex cells (one point per cell). 884 If False, the input vertex cells will be ignored. 885 lines : (bool) 886 if True, break input polylines into line segments. 887 If False, input lines will be ignored and the output will have no lines. 888 """ 889 if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips(): 890 # print("Using vtkTriangleFilter") 891 tf = vtki.new("TriangleFilter") 892 tf.SetPassLines(lines) 893 tf.SetPassVerts(verts) 894 895 elif self.dataset.GetNumberOfLines(): 896 # print("Using vtkContourTriangulator") 897 tf = vtki.new("ContourTriangulator") 898 tf.TriangulationErrorDisplayOn() 899 900 else: 901 vedo.logger.debug("input in triangulate() seems to be void! Skip.") 902 return self 903 904 tf.SetInputData(self.dataset) 905 tf.Update() 906 self._update(tf.GetOutput(), reset_locators=False) 907 self.lw(0).lighting("default").pickable() 908 909 self.pipeline = OperationNode( 910 "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}" 911 ) 912 return self 913 914 def compute_cell_vertex_count(self) -> Self: 915 """ 916 Add to this mesh a cell data array containing the nr of vertices that a polygonal face has. 917 """ 918 csf = vtki.new("CellSizeFilter") 919 csf.SetInputData(self.dataset) 920 csf.SetComputeArea(False) 921 csf.SetComputeVolume(False) 922 csf.SetComputeLength(False) 923 csf.SetComputeVertexCount(True) 924 csf.SetVertexCountArrayName("VertexCount") 925 csf.Update() 926 self.dataset.GetCellData().AddArray( 927 csf.GetOutput().GetCellData().GetArray("VertexCount") 928 ) 929 return self 930 931 def compute_quality(self, metric=6) -> Self: 932 """ 933 Calculate metrics of quality for the elements of a triangular mesh. 934 This method adds to the mesh a cell array named "Quality". 935 See class 936 [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html). 937 938 Arguments: 939 metric : (int) 940 type of available estimators are: 941 - EDGE RATIO, 0 942 - ASPECT RATIO, 1 943 - RADIUS RATIO, 2 944 - ASPECT FROBENIUS, 3 945 - MED ASPECT FROBENIUS, 4 946 - MAX ASPECT FROBENIUS, 5 947 - MIN_ANGLE, 6 948 - COLLAPSE RATIO, 7 949 - MAX ANGLE, 8 950 - CONDITION, 9 951 - SCALED JACOBIAN, 10 952 - SHEAR, 11 953 - RELATIVE SIZE SQUARED, 12 954 - SHAPE, 13 955 - SHAPE AND SIZE, 14 956 - DISTORTION, 15 957 - MAX EDGE RATIO, 16 958 - SKEW, 17 959 - TAPER, 18 960 - VOLUME, 19 961 - STRETCH, 20 962 - DIAGONAL, 21 963 - DIMENSION, 22 964 - ODDY, 23 965 - SHEAR AND SIZE, 24 966 - JACOBIAN, 25 967 - WARPAGE, 26 968 - ASPECT GAMMA, 27 969 - AREA, 28 970 - ASPECT BETA, 29 971 972 Examples: 973 - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py) 974 975 ![](https://vedo.embl.es/images/advanced/meshquality.png) 976 """ 977 qf = vtki.new("MeshQuality") 978 qf.SetInputData(self.dataset) 979 qf.SetTriangleQualityMeasure(metric) 980 qf.SaveCellQualityOn() 981 qf.Update() 982 self._update(qf.GetOutput(), reset_locators=False) 983 self.mapper.SetScalarModeToUseCellData() 984 self.pipeline = OperationNode("compute_quality", parents=[self]) 985 return self 986 987 def count_vertices(self) -> np.ndarray: 988 """Count the number of vertices each cell has and return it as a numpy array""" 989 vc = vtki.new("CountVertices") 990 vc.SetInputData(self.dataset) 991 vc.SetOutputArrayName("VertexCount") 992 vc.Update() 993 varr = vc.GetOutput().GetCellData().GetArray("VertexCount") 994 return vtk2numpy(varr) 995 996 def check_validity(self, tol=0) -> np.ndarray: 997 """ 998 Return a numpy array of possible problematic faces following this convention: 999 - Valid = 0 1000 - WrongNumberOfPoints = 1 1001 - IntersectingEdges = 2 1002 - IntersectingFaces = 4 1003 - NoncontiguousEdges = 8 1004 - Nonconvex = 10 1005 - OrientedIncorrectly = 20 1006 1007 Arguments: 1008 tol : (float) 1009 value is used as an epsilon for floating point 1010 equality checks throughout the cell checking process. 1011 """ 1012 vald = vtki.new("CellValidator") 1013 if tol: 1014 vald.SetTolerance(tol) 1015 vald.SetInputData(self.dataset) 1016 vald.Update() 1017 varr = vald.GetOutput().GetCellData().GetArray("ValidityState") 1018 return vtk2numpy(varr) 1019 1020 def compute_curvature(self, method=0) -> Self: 1021 """ 1022 Add scalars to `Mesh` that contains the curvature calculated in three different ways. 1023 1024 Variable `method` can be: 1025 - 0 = gaussian 1026 - 1 = mean curvature 1027 - 2 = max curvature 1028 - 3 = min curvature 1029 1030 Example: 1031 ```python 1032 from vedo import Torus 1033 Torus().compute_curvature().add_scalarbar().show().close() 1034 ``` 1035 ![](https://vedo.embl.es/images/advanced/torus_curv.png) 1036 """ 1037 curve = vtki.new("Curvatures") 1038 curve.SetInputData(self.dataset) 1039 curve.SetCurvatureType(method) 1040 curve.Update() 1041 self._update(curve.GetOutput(), reset_locators=False) 1042 self.mapper.ScalarVisibilityOn() 1043 return self 1044 1045 def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self: 1046 """ 1047 Add to `Mesh` a scalar array that contains distance along a specified direction. 1048 1049 Arguments: 1050 low : (list) 1051 one end of the line (small scalar values) 1052 high : (list) 1053 other end of the line (large scalar values) 1054 vrange : (list) 1055 set the range of the scalar 1056 1057 Example: 1058 ```python 1059 from vedo import Sphere 1060 s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1)) 1061 s.add_scalarbar().show(axes=1).close() 1062 ``` 1063 ![](https://vedo.embl.es/images/basic/compute_elevation.png) 1064 """ 1065 ef = vtki.new("ElevationFilter") 1066 ef.SetInputData(self.dataset) 1067 ef.SetLowPoint(low) 1068 ef.SetHighPoint(high) 1069 ef.SetScalarRange(vrange) 1070 ef.Update() 1071 self._update(ef.GetOutput(), reset_locators=False) 1072 self.mapper.ScalarVisibilityOn() 1073 return self 1074 1075 1076 def laplacian_diffusion(self, array_name, dt, num_steps) -> Self: 1077 """ 1078 Apply a diffusion process to a scalar array defined on the points of a mesh. 1079 1080 Arguments: 1081 array_name : (str) 1082 name of the array to diffuse. 1083 dt : (float) 1084 time step. 1085 num_steps : (int) 1086 number of iterations. 1087 """ 1088 try: 1089 import scipy.sparse 1090 import scipy.sparse.linalg 1091 except ImportError: 1092 vedo.logger.error("scipy not found. Cannot run laplacian_diffusion()") 1093 return self 1094 1095 def build_laplacian(): 1096 rows = [] 1097 cols = [] 1098 data = [] 1099 n_points = points.shape[0] 1100 avg_area = np.mean(areas) * 10000 1101 # print("avg_area", avg_area) 1102 1103 for triangle in cells: 1104 for i in range(3): 1105 for j in range(i + 1, 3): 1106 u = triangle[i] 1107 v = triangle[j] 1108 rows.append(u) 1109 cols.append(v) 1110 rows.append(v) 1111 cols.append(u) 1112 data.append(-1/avg_area) 1113 data.append(-1/avg_area) 1114 1115 L = scipy.sparse.coo_matrix( 1116 (data, (rows, cols)), shape=(n_points, n_points) 1117 ).tocsc() 1118 1119 degree = -np.array(L.sum(axis=1)).flatten() # adjust the diagonal 1120 # print("degree", degree) 1121 L.setdiag(degree) 1122 return L 1123 1124 def _diffuse(u0, L, dt, num_steps): 1125 # mean_area = np.mean(areas) * 10000 1126 # print("mean_area", mean_area) 1127 mean_area = 1 1128 I = scipy.sparse.eye(L.shape[0], format="csc") 1129 A = I - (dt/mean_area) * L 1130 u = u0 1131 for _ in range(int(num_steps)): 1132 u = A.dot(u) 1133 return u 1134 1135 self.compute_cell_size() 1136 areas = self.celldata["Area"] 1137 points = self.vertices 1138 cells = self.cells 1139 u0 = self.pointdata[array_name] 1140 1141 # Simulate diffusion 1142 L = build_laplacian() 1143 u = _diffuse(u0, L, dt, num_steps) 1144 self.pointdata[array_name] = u 1145 return self 1146 1147 1148 def subdivide(self, n=1, method=0, mel=None) -> Self: 1149 """ 1150 Increase the number of vertices of a surface mesh. 1151 1152 Arguments: 1153 n : (int) 1154 number of subdivisions. 1155 method : (int) 1156 Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4) 1157 mel : (float) 1158 Maximum Edge Length (applicable to Adaptive method only). 1159 """ 1160 triangles = vtki.new("TriangleFilter") 1161 triangles.SetInputData(self.dataset) 1162 triangles.Update() 1163 tri_mesh = triangles.GetOutput() 1164 if method == 0: 1165 sdf = vtki.new("LoopSubdivisionFilter") 1166 elif method == 1: 1167 sdf = vtki.new("LinearSubdivisionFilter") 1168 elif method == 2: 1169 sdf = vtki.new("AdaptiveSubdivisionFilter") 1170 if mel is None: 1171 mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n 1172 sdf.SetMaximumEdgeLength(mel) 1173 elif method == 3: 1174 sdf = vtki.new("ButterflySubdivisionFilter") 1175 elif method == 4: 1176 sdf = vtki.new("DensifyPolyData") 1177 else: 1178 vedo.logger.error(f"in subdivide() unknown method {method}") 1179 raise RuntimeError() 1180 1181 if method != 2: 1182 sdf.SetNumberOfSubdivisions(n) 1183 1184 sdf.SetInputData(tri_mesh) 1185 sdf.Update() 1186 1187 self._update(sdf.GetOutput()) 1188 1189 self.pipeline = OperationNode( 1190 "subdivide", 1191 parents=[self], 1192 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1193 ) 1194 return self 1195 1196 1197 def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self: 1198 """ 1199 Downsample the number of vertices in a mesh to `fraction`. 1200 1201 This filter preserves the `pointdata` of the input dataset. In previous versions 1202 of vedo, this decimation algorithm was referred to as quadric decimation. 1203 1204 Arguments: 1205 fraction : (float) 1206 the desired target of reduction. 1207 n : (int) 1208 the desired number of final points 1209 (`fraction` is recalculated based on it). 1210 preserve_volume : (bool) 1211 Decide whether to activate volume preservation which greatly 1212 reduces errors in triangle normal direction. 1213 regularization : (float) 1214 regularize the point finding algorithm so as to have better quality 1215 mesh elements at the cost of a slightly lower precision on the 1216 geometry potentially (mostly at sharp edges). 1217 Can be useful for decimating meshes that have been triangulated on noisy data. 1218 1219 Note: 1220 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1221 Internally the VTK class 1222 [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html) 1223 is used for this operation. 1224 1225 See also: `decimate_binned()` and `decimate_pro()`. 1226 """ 1227 poly = self.dataset 1228 if n: # N = desired number of points 1229 npt = poly.GetNumberOfPoints() 1230 fraction = n / npt 1231 if fraction >= 1: 1232 return self 1233 1234 decimate = vtki.new("QuadricDecimation") 1235 decimate.SetVolumePreservation(preserve_volume) 1236 # decimate.AttributeErrorMetricOn() 1237 if regularization: 1238 decimate.SetRegularize(True) 1239 decimate.SetRegularization(regularization) 1240 1241 try: 1242 decimate.MapPointDataOn() 1243 except AttributeError: 1244 pass 1245 1246 decimate.SetTargetReduction(1 - fraction) 1247 decimate.SetInputData(poly) 1248 decimate.Update() 1249 1250 self._update(decimate.GetOutput()) 1251 self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction() 1252 1253 self.pipeline = OperationNode( 1254 "decimate", 1255 parents=[self], 1256 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1257 ) 1258 return self 1259 1260 def decimate_pro( 1261 self, 1262 fraction=0.5, 1263 n=None, 1264 preserve_topology=True, 1265 preserve_boundaries=True, 1266 splitting=False, 1267 splitting_angle=75, 1268 feature_angle=0, 1269 inflection_point_ratio=10, 1270 vertex_degree=0, 1271 ) -> Self: 1272 """ 1273 Downsample the number of vertices in a mesh to `fraction`. 1274 1275 This filter preserves the `pointdata` of the input dataset. 1276 1277 Arguments: 1278 fraction : (float) 1279 The desired target of reduction. 1280 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1281 n : (int) 1282 the desired number of final points (`fraction` is recalculated based on it). 1283 preserve_topology : (bool) 1284 If on, mesh splitting and hole elimination will not occur. 1285 This may limit the maximum reduction that may be achieved. 1286 preserve_boundaries : (bool) 1287 Turn on/off the deletion of vertices on the boundary of a mesh. 1288 Control whether mesh boundaries are preserved during decimation. 1289 feature_angle : (float) 1290 Specify the angle that defines a feature. 1291 This angle is used to define what an edge is 1292 (i.e., if the surface normal between two adjacent triangles 1293 is >= FeatureAngle, an edge exists). 1294 splitting : (bool) 1295 Turn on/off the splitting of the mesh at corners, 1296 along edges, at non-manifold points, or anywhere else a split is required. 1297 Turning splitting off will better preserve the original topology of the mesh, 1298 but you may not obtain the requested reduction. 1299 splitting_angle : (float) 1300 Specify the angle that defines a sharp edge. 1301 This angle is used to control the splitting of the mesh. 1302 A split line exists when the surface normals between two edge connected triangles 1303 are >= `splitting_angle`. 1304 inflection_point_ratio : (float) 1305 An inflection point occurs when the ratio of reduction error between two iterations 1306 is greater than or equal to the `inflection_point_ratio` value. 1307 vertex_degree : (int) 1308 If the number of triangles connected to a vertex exceeds it then the vertex will be split. 1309 1310 Note: 1311 Setting `fraction=0.1` leaves 10% of the original number of vertices 1312 1313 See also: 1314 `decimate()` and `decimate_binned()`. 1315 """ 1316 poly = self.dataset 1317 if n: # N = desired number of points 1318 npt = poly.GetNumberOfPoints() 1319 fraction = n / npt 1320 if fraction >= 1: 1321 return self 1322 1323 decimate = vtki.new("DecimatePro") 1324 decimate.SetPreserveTopology(preserve_topology) 1325 decimate.SetBoundaryVertexDeletion(preserve_boundaries) 1326 if feature_angle: 1327 decimate.SetFeatureAngle(feature_angle) 1328 decimate.SetSplitting(splitting) 1329 decimate.SetSplitAngle(splitting_angle) 1330 decimate.SetInflectionPointRatio(inflection_point_ratio) 1331 if vertex_degree: 1332 decimate.SetDegree(vertex_degree) 1333 1334 decimate.SetTargetReduction(1 - fraction) 1335 decimate.SetInputData(poly) 1336 decimate.Update() 1337 self._update(decimate.GetOutput()) 1338 1339 self.pipeline = OperationNode( 1340 "decimate_pro", 1341 parents=[self], 1342 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1343 ) 1344 return self 1345 1346 def decimate_binned(self, divisions=(), use_clustering=False) -> Self: 1347 """ 1348 Downsample the number of vertices in a mesh. 1349 1350 This filter preserves the `celldata` of the input dataset, 1351 if `use_clustering=True` also the `pointdata` will be preserved in the result. 1352 1353 Arguments: 1354 divisions : (list) 1355 number of divisions along x, y and z axes. 1356 auto_adjust : (bool) 1357 if True, the number of divisions is automatically adjusted to 1358 create more uniform cells. 1359 use_clustering : (bool) 1360 use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html) 1361 instead of 1362 [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html). 1363 1364 See also: `decimate()` and `decimate_pro()`. 1365 """ 1366 if use_clustering: 1367 decimate = vtki.new("QuadricClustering") 1368 decimate.CopyCellDataOn() 1369 else: 1370 decimate = vtki.new("BinnedDecimation") 1371 decimate.ProducePointDataOn() 1372 decimate.ProduceCellDataOn() 1373 1374 decimate.SetInputData(self.dataset) 1375 1376 if len(divisions) == 0: 1377 decimate.SetAutoAdjustNumberOfDivisions(1) 1378 else: 1379 decimate.SetAutoAdjustNumberOfDivisions(0) 1380 decimate.SetNumberOfDivisions(divisions) 1381 decimate.Update() 1382 1383 self._update(decimate.GetOutput()) 1384 self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions() 1385 self.pipeline = OperationNode( 1386 "decimate_binned", 1387 parents=[self], 1388 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1389 ) 1390 return self 1391 1392 def generate_random_points(self, n: int, min_radius=0.0) -> "Points": 1393 """ 1394 Generate `n` uniformly distributed random points 1395 inside the polygonal mesh. 1396 1397 A new point data array is added to the output points 1398 called "OriginalCellID" which contains the index of 1399 the cell ID in which the point was generated. 1400 1401 Arguments: 1402 n : (int) 1403 number of points to generate. 1404 min_radius: (float) 1405 impose a minimum distance between points. 1406 If `min_radius` is set to 0, the points are 1407 generated uniformly at random inside the mesh. 1408 If `min_radius` is set to a positive value, 1409 the points are generated uniformly at random 1410 inside the mesh, but points closer than `min_radius` 1411 to any other point are discarded. 1412 1413 Returns a `vedo.Points` object. 1414 1415 Note: 1416 Consider using `points.probe(msh)` or 1417 `points.interpolate_data_from(msh)` 1418 to interpolate existing mesh data onto the new points. 1419 1420 Example: 1421 ```python 1422 from vedo import * 1423 msh = Mesh(dataurl + "panther.stl").lw(2) 1424 pts = msh.generate_random_points(20000, min_radius=0.5) 1425 print("Original cell ids:", pts.pointdata["OriginalCellID"]) 1426 show(pts, msh, axes=1).close() 1427 ``` 1428 """ 1429 cmesh = self.clone().clean().triangulate().compute_cell_size() 1430 triangles = cmesh.cells 1431 vertices = cmesh.vertices 1432 cumul = np.cumsum(cmesh.celldata["Area"]) 1433 1434 out_pts = [] 1435 orig_cell = [] 1436 for _ in range(n): 1437 # choose a triangle based on area 1438 random_area = np.random.random() * cumul[-1] 1439 it = np.searchsorted(cumul, random_area) 1440 A, B, C = vertices[triangles[it]] 1441 # calculate the random point in the triangle 1442 r1, r2 = np.random.random(2) 1443 if r1 + r2 > 1: 1444 r1 = 1 - r1 1445 r2 = 1 - r2 1446 out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C) 1447 orig_cell.append(it) 1448 nporig_cell = np.array(orig_cell, dtype=np.uint32) 1449 1450 vpts = Points(out_pts) 1451 vpts.pointdata["OriginalCellID"] = nporig_cell 1452 1453 if min_radius > 0: 1454 vpts.subsample(min_radius, absolute=True) 1455 1456 vpts.point_size(5).color("k1") 1457 vpts.name = "RandomPoints" 1458 vpts.pipeline = OperationNode( 1459 "generate_random_points", c="#edabab", parents=[self]) 1460 return vpts 1461 1462 def delete_cells(self, ids: List[int]) -> Self: 1463 """ 1464 Remove cells from the mesh object by their ID. 1465 Points (vertices) are not removed (you may use `clean()` to remove those). 1466 """ 1467 self.dataset.BuildLinks() 1468 for cid in ids: 1469 self.dataset.DeleteCell(cid) 1470 self.dataset.RemoveDeletedCells() 1471 self.dataset.Modified() 1472 self.mapper.Modified() 1473 self.pipeline = OperationNode( 1474 "delete_cells", 1475 parents=[self], 1476 comment=f"#cells {self.dataset.GetNumberOfCells()}", 1477 ) 1478 return self 1479 1480 def delete_cells_by_point_index(self, indices: List[int]) -> Self: 1481 """ 1482 Delete a list of vertices identified by any of their vertex index. 1483 1484 See also `delete_cells()`. 1485 1486 Examples: 1487 - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py) 1488 1489 ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png) 1490 """ 1491 cell_ids = vtki.vtkIdList() 1492 self.dataset.BuildLinks() 1493 n = 0 1494 for i in np.unique(indices): 1495 self.dataset.GetPointCells(i, cell_ids) 1496 for j in range(cell_ids.GetNumberOfIds()): 1497 self.dataset.DeleteCell(cell_ids.GetId(j)) # flag cell 1498 n += 1 1499 1500 self.dataset.RemoveDeletedCells() 1501 self.dataset.Modified() 1502 self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self]) 1503 return self 1504 1505 def collapse_edges(self, distance: float, iterations=1) -> Self: 1506 """ 1507 Collapse mesh edges so that are all above `distance`. 1508 1509 Example: 1510 ```python 1511 from vedo import * 1512 np.random.seed(2) 1513 grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1) 1514 grid1.celldata['scalar'] = grid1.cell_centers[:,1] 1515 grid2 = grid1.clone().collapse_edges(0.1) 1516 show(grid1, grid2, N=2, axes=1) 1517 ``` 1518 """ 1519 for _ in range(iterations): 1520 medges = self.edges 1521 pts = self.vertices 1522 newpts = np.array(pts) 1523 moved = [] 1524 for e in medges: 1525 if len(e) == 2: 1526 id0, id1 = e 1527 p0, p1 = pts[id0], pts[id1] 1528 if (np.linalg.norm(p1-p0) < distance 1529 and id0 not in moved 1530 and id1 not in moved 1531 ): 1532 p = (p0 + p1) / 2 1533 newpts[id0] = p 1534 newpts[id1] = p 1535 moved += [id0, id1] 1536 self.vertices = newpts 1537 cpd = vtki.new("CleanPolyData") 1538 cpd.ConvertLinesToPointsOff() 1539 cpd.ConvertPolysToLinesOff() 1540 cpd.ConvertStripsToPolysOff() 1541 cpd.SetInputData(self.dataset) 1542 cpd.Update() 1543 self._update(cpd.GetOutput()) 1544 1545 self.pipeline = OperationNode( 1546 "collapse_edges", 1547 parents=[self], 1548 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1549 ) 1550 return self 1551 1552 def adjacency_list(self) -> List[set]: 1553 """ 1554 Computes the adjacency list for mesh edge-graph. 1555 1556 Returns: 1557 a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex 1558 """ 1559 inc = [set()] * self.nvertices 1560 for cell in self.cells: 1561 nc = len(cell) 1562 if nc > 1: 1563 for i in range(nc-1): 1564 ci = cell[i] 1565 inc[ci] = inc[ci].union({cell[i-1], cell[i+1]}) 1566 return inc 1567 1568 def graph_ball(self, index, n: int) -> set: 1569 """ 1570 Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`. 1571 1572 Arguments: 1573 index : (int) 1574 index of the vertex 1575 n : (int) 1576 radius in the graph metric 1577 1578 Returns: 1579 the set of indices of the vertices which are at most `n` edges from vertex `index`. 1580 """ 1581 if n == 0: 1582 return {index} 1583 else: 1584 al = self.adjacency_list() 1585 ball = {index} 1586 i = 0 1587 while i < n and len(ball) < self.nvertices: 1588 for v in ball: 1589 ball = ball.union(al[v]) 1590 i += 1 1591 return ball 1592 1593 def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self: 1594 """ 1595 Adjust mesh point positions using the so-called "Windowed Sinc" method. 1596 1597 Arguments: 1598 niter : (int) 1599 number of iterations. 1600 pass_band : (float) 1601 set the pass_band value for the windowed sinc filter. 1602 edge_angle : (float) 1603 edge angle to control smoothing along edges (either interior or boundary). 1604 feature_angle : (float) 1605 specifies the feature angle for sharp edge identification. 1606 boundary : (bool) 1607 specify if boundary should also be smoothed or kept unmodified 1608 1609 Examples: 1610 - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py) 1611 1612 ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png) 1613 """ 1614 cl = vtki.new("CleanPolyData") 1615 cl.SetInputData(self.dataset) 1616 cl.Update() 1617 smf = vtki.new("WindowedSincPolyDataFilter") 1618 smf.SetInputData(cl.GetOutput()) 1619 smf.SetNumberOfIterations(niter) 1620 smf.SetEdgeAngle(edge_angle) 1621 smf.SetFeatureAngle(feature_angle) 1622 smf.SetPassBand(pass_band) 1623 smf.NormalizeCoordinatesOn() 1624 smf.NonManifoldSmoothingOn() 1625 smf.FeatureEdgeSmoothingOn() 1626 smf.SetBoundarySmoothing(boundary) 1627 smf.Update() 1628 1629 self._update(smf.GetOutput()) 1630 1631 self.pipeline = OperationNode( 1632 "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1633 ) 1634 return self 1635 1636 def fill_holes(self, size=None) -> Self: 1637 """ 1638 Identifies and fills holes in the input mesh. 1639 Holes are identified by locating boundary edges, linking them together 1640 into loops, and then triangulating the resulting loops. 1641 1642 Arguments: 1643 size : (float) 1644 Approximate limit to the size of the hole that can be filled. 1645 1646 Examples: 1647 - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py) 1648 """ 1649 fh = vtki.new("FillHolesFilter") 1650 if not size: 1651 mb = self.diagonal_size() 1652 size = mb / 10 1653 fh.SetHoleSize(size) 1654 fh.SetInputData(self.dataset) 1655 fh.Update() 1656 1657 self._update(fh.GetOutput()) 1658 1659 self.pipeline = OperationNode( 1660 "fill_holes", 1661 parents=[self], 1662 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1663 ) 1664 return self 1665 1666 def contains(self, point: tuple, tol=1e-05) -> bool: 1667 """ 1668 Return True if point is inside a polydata closed surface. 1669 1670 Note: 1671 if you have many points to check use `inside_points()` instead. 1672 1673 Example: 1674 ```python 1675 from vedo import * 1676 s = Sphere().c('green5').alpha(0.5) 1677 pt = [0.1, 0.2, 0.3] 1678 print("Sphere contains", pt, s.contains(pt)) 1679 show(s, Point(pt), axes=1).close() 1680 ``` 1681 """ 1682 points = vtki.vtkPoints() 1683 points.InsertNextPoint(point) 1684 poly = vtki.vtkPolyData() 1685 poly.SetPoints(points) 1686 sep = vtki.new("SelectEnclosedPoints") 1687 sep.SetTolerance(tol) 1688 sep.CheckSurfaceOff() 1689 sep.SetInputData(poly) 1690 sep.SetSurfaceData(self.dataset) 1691 sep.Update() 1692 return bool(sep.IsInside(0)) 1693 1694 def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]: 1695 """ 1696 Return the point cloud that is inside mesh surface as a new Points object. 1697 1698 If return_ids is True a list of IDs is returned and in addition input points 1699 are marked by a pointdata array named "IsInside". 1700 1701 Example: 1702 `print(pts.pointdata["IsInside"])` 1703 1704 Examples: 1705 - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 1706 1707 ![](https://vedo.embl.es/images/basic/pca.png) 1708 """ 1709 if isinstance(pts, Points): 1710 poly = pts.dataset 1711 ptsa = pts.vertices 1712 else: 1713 ptsa = np.asarray(pts) 1714 vpoints = vtki.vtkPoints() 1715 vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32)) 1716 poly = vtki.vtkPolyData() 1717 poly.SetPoints(vpoints) 1718 1719 sep = vtki.new("SelectEnclosedPoints") 1720 # sep = vtki.new("ExtractEnclosedPoints() 1721 sep.SetTolerance(tol) 1722 sep.SetInputData(poly) 1723 sep.SetSurfaceData(self.dataset) 1724 sep.SetInsideOut(invert) 1725 sep.Update() 1726 1727 varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints") 1728 mask = vtk2numpy(varr).astype(bool) 1729 ids = np.array(range(len(ptsa)), dtype=int)[mask] 1730 1731 if isinstance(pts, Points): 1732 varr.SetName("IsInside") 1733 pts.dataset.GetPointData().AddArray(varr) 1734 1735 if return_ids: 1736 return ids 1737 1738 pcl = Points(ptsa[ids]) 1739 pcl.name = "InsidePoints" 1740 1741 pcl.pipeline = OperationNode( 1742 "inside_points", 1743 parents=[self, ptsa], 1744 comment=f"#pts {pcl.dataset.GetNumberOfPoints()}", 1745 ) 1746 return pcl 1747 1748 def boundaries( 1749 self, 1750 boundary_edges=True, 1751 manifold_edges=False, 1752 non_manifold_edges=False, 1753 feature_angle=None, 1754 return_point_ids=False, 1755 return_cell_ids=False, 1756 cell_edge=False, 1757 ) -> Union[Self, np.ndarray]: 1758 """ 1759 Return the boundary lines of an input mesh. 1760 Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method. 1761 1762 Arguments: 1763 boundary_edges : (bool) 1764 Turn on/off the extraction of boundary edges. 1765 manifold_edges : (bool) 1766 Turn on/off the extraction of manifold edges. 1767 non_manifold_edges : (bool) 1768 Turn on/off the extraction of non-manifold edges. 1769 feature_angle : (bool) 1770 Specify the min angle btw 2 faces for extracting edges. 1771 return_point_ids : (bool) 1772 return a numpy array of point indices 1773 return_cell_ids : (bool) 1774 return a numpy array of cell indices 1775 cell_edge : (bool) 1776 set to `True` if a cell need to share an edge with 1777 the boundary line, or `False` if a single vertex is enough 1778 1779 Examples: 1780 - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py) 1781 1782 ![](https://vedo.embl.es/images/basic/boundaries.png) 1783 """ 1784 fe = vtki.new("FeatureEdges") 1785 fe.SetBoundaryEdges(boundary_edges) 1786 fe.SetNonManifoldEdges(non_manifold_edges) 1787 fe.SetManifoldEdges(manifold_edges) 1788 try: 1789 fe.SetPassLines(True) # vtk9.2 1790 except AttributeError: 1791 pass 1792 fe.ColoringOff() 1793 fe.SetFeatureEdges(False) 1794 if feature_angle is not None: 1795 fe.SetFeatureEdges(True) 1796 fe.SetFeatureAngle(feature_angle) 1797 1798 if return_point_ids or return_cell_ids: 1799 idf = vtki.new("IdFilter") 1800 idf.SetInputData(self.dataset) 1801 idf.SetPointIdsArrayName("BoundaryIds") 1802 idf.SetPointIds(True) 1803 idf.Update() 1804 1805 fe.SetInputData(idf.GetOutput()) 1806 fe.Update() 1807 1808 vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds") 1809 npid = vtk2numpy(vid).astype(int) 1810 1811 if return_point_ids: 1812 return npid 1813 1814 if return_cell_ids: 1815 n = 1 if cell_edge else 0 1816 inface = [] 1817 for i, face in enumerate(self.cells): 1818 # isin = np.any([vtx in npid for vtx in face]) 1819 isin = 0 1820 for vtx in face: 1821 isin += int(vtx in npid) 1822 if isin > n: 1823 break 1824 if isin > n: 1825 inface.append(i) 1826 return np.array(inface).astype(int) 1827 1828 return self 1829 1830 else: 1831 1832 fe.SetInputData(self.dataset) 1833 fe.Update() 1834 msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off") 1835 msh.name = "MeshBoundaries" 1836 1837 msh.pipeline = OperationNode( 1838 "boundaries", 1839 parents=[self], 1840 shape="octagon", 1841 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 1842 ) 1843 return msh 1844 1845 def imprint(self, loopline, tol=0.01) -> Self: 1846 """ 1847 Imprint the contact surface of one object onto another surface. 1848 1849 Arguments: 1850 loopline : (vedo.Line) 1851 a Line object to be imprinted onto the mesh. 1852 tol : (float) 1853 projection tolerance which controls how close the imprint 1854 surface must be to the target. 1855 1856 Example: 1857 ```python 1858 from vedo import * 1859 grid = Grid()#.triangulate() 1860 circle = Circle(r=0.3, res=24).pos(0.11,0.12) 1861 line = Line(circle, closed=True, lw=4, c='r4') 1862 grid.imprint(line) 1863 show(grid, line, axes=1).close() 1864 ``` 1865 ![](https://vedo.embl.es/images/feats/imprint.png) 1866 """ 1867 loop = vtki.new("ContourLoopExtraction") 1868 loop.SetInputData(loopline.dataset) 1869 loop.Update() 1870 1871 clean_loop = vtki.new("CleanPolyData") 1872 clean_loop.SetInputData(loop.GetOutput()) 1873 clean_loop.Update() 1874 1875 imp = vtki.new("ImprintFilter") 1876 imp.SetTargetData(self.dataset) 1877 imp.SetImprintData(clean_loop.GetOutput()) 1878 imp.SetTolerance(tol) 1879 imp.BoundaryEdgeInsertionOn() 1880 imp.TriangulateOutputOn() 1881 imp.Update() 1882 1883 self._update(imp.GetOutput()) 1884 1885 self.pipeline = OperationNode( 1886 "imprint", 1887 parents=[self], 1888 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1889 ) 1890 return self 1891 1892 def connected_vertices(self, index: int) -> List[int]: 1893 """Find all vertices connected to an input vertex specified by its index. 1894 1895 Examples: 1896 - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py) 1897 1898 ![](https://vedo.embl.es/images/basic/connVtx.png) 1899 """ 1900 poly = self.dataset 1901 1902 cell_idlist = vtki.vtkIdList() 1903 poly.GetPointCells(index, cell_idlist) 1904 1905 idxs = [] 1906 for i in range(cell_idlist.GetNumberOfIds()): 1907 point_idlist = vtki.vtkIdList() 1908 poly.GetCellPoints(cell_idlist.GetId(i), point_idlist) 1909 for j in range(point_idlist.GetNumberOfIds()): 1910 idj = point_idlist.GetId(j) 1911 if idj == index: 1912 continue 1913 if idj in idxs: 1914 continue 1915 idxs.append(idj) 1916 1917 return idxs 1918 1919 def extract_cells(self, ids: List[int]) -> Self: 1920 """ 1921 Extract a subset of cells from a mesh and return it as a new mesh. 1922 """ 1923 selectCells = vtki.new("SelectionNode") 1924 selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL) 1925 selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES) 1926 idarr = vtki.vtkIdTypeArray() 1927 idarr.SetNumberOfComponents(1) 1928 idarr.SetNumberOfValues(len(ids)) 1929 for i, v in enumerate(ids): 1930 idarr.SetValue(i, v) 1931 selectCells.SetSelectionList(idarr) 1932 1933 selection = vtki.new("Selection") 1934 selection.AddNode(selectCells) 1935 1936 extractSelection = vtki.new("ExtractSelection") 1937 extractSelection.SetInputData(0, self.dataset) 1938 extractSelection.SetInputData(1, selection) 1939 extractSelection.Update() 1940 1941 gf = vtki.new("GeometryFilter") 1942 gf.SetInputData(extractSelection.GetOutput()) 1943 gf.Update() 1944 msh = Mesh(gf.GetOutput()) 1945 msh.copy_properties_from(self) 1946 return msh 1947 1948 def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]: 1949 """Find all cellls connected to an input vertex specified by its index.""" 1950 1951 # Find all cells connected to point index 1952 dpoly = self.dataset 1953 idlist = vtki.vtkIdList() 1954 dpoly.GetPointCells(index, idlist) 1955 1956 ids = vtki.vtkIdTypeArray() 1957 ids.SetNumberOfComponents(1) 1958 rids = [] 1959 for k in range(idlist.GetNumberOfIds()): 1960 cid = idlist.GetId(k) 1961 ids.InsertNextValue(cid) 1962 rids.append(int(cid)) 1963 if return_ids: 1964 return rids 1965 1966 selection_node = vtki.new("SelectionNode") 1967 selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL) 1968 selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES) 1969 selection_node.SetSelectionList(ids) 1970 selection = vtki.new("Selection") 1971 selection.AddNode(selection_node) 1972 extractSelection = vtki.new("ExtractSelection") 1973 extractSelection.SetInputData(0, dpoly) 1974 extractSelection.SetInputData(1, selection) 1975 extractSelection.Update() 1976 gf = vtki.new("GeometryFilter") 1977 gf.SetInputData(extractSelection.GetOutput()) 1978 gf.Update() 1979 return Mesh(gf.GetOutput()).lw(1) 1980 1981 def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self: 1982 """ 1983 Return a new line `Mesh` which corresponds to the outer `silhouette` 1984 of the input as seen along a specified `direction`, this can also be 1985 a `vtkCamera` object. 1986 1987 Arguments: 1988 direction : (list) 1989 viewpoint direction vector. 1990 If `None` this is guessed by looking at the minimum 1991 of the sides of the bounding box. 1992 border_edges : (bool) 1993 enable or disable generation of border edges 1994 feature_angle : (float) 1995 minimal angle for sharp edges detection. 1996 If set to `False` the functionality is disabled. 1997 1998 Examples: 1999 - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py) 2000 2001 ![](https://vedo.embl.es/images/basic/silhouette1.png) 2002 """ 2003 sil = vtki.new("PolyDataSilhouette") 2004 sil.SetInputData(self.dataset) 2005 sil.SetBorderEdges(border_edges) 2006 if feature_angle is False: 2007 sil.SetEnableFeatureAngle(0) 2008 else: 2009 sil.SetEnableFeatureAngle(1) 2010 sil.SetFeatureAngle(feature_angle) 2011 2012 if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera: 2013 sil.SetCamera(vedo.plotter_instance.camera) 2014 m = Mesh() 2015 m.mapper.SetInputConnection(sil.GetOutputPort()) 2016 2017 elif isinstance(direction, vtki.vtkCamera): 2018 sil.SetCamera(direction) 2019 m = Mesh() 2020 m.mapper.SetInputConnection(sil.GetOutputPort()) 2021 2022 elif direction == "2d": 2023 sil.SetVector(3.4, 4.5, 5.6) # random 2024 sil.SetDirectionToSpecifiedVector() 2025 sil.Update() 2026 m = Mesh(sil.GetOutput()) 2027 2028 elif is_sequence(direction): 2029 sil.SetVector(direction) 2030 sil.SetDirectionToSpecifiedVector() 2031 sil.Update() 2032 m = Mesh(sil.GetOutput()) 2033 else: 2034 vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}") 2035 vedo.logger.error("first render the scene with show() or specify camera/direction") 2036 return self 2037 2038 m.lw(2).c((0, 0, 0)).lighting("off") 2039 m.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2040 m.pipeline = OperationNode("silhouette", parents=[self]) 2041 m.name = "Silhouette" 2042 return m 2043 2044 def isobands(self, n=10, vmin=None, vmax=None) -> Self: 2045 """ 2046 Return a new `Mesh` representing the isobands of the active scalars. 2047 This is a new mesh where the scalar is now associated to cell faces and 2048 used to colorize the mesh. 2049 2050 Arguments: 2051 n : (int) 2052 number of isobands in the range 2053 vmin : (float) 2054 minimum of the range 2055 vmax : (float) 2056 maximum of the range 2057 2058 Examples: 2059 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2060 """ 2061 r0, r1 = self.dataset.GetScalarRange() 2062 if vmin is None: 2063 vmin = r0 2064 if vmax is None: 2065 vmax = r1 2066 2067 # -------------------------------- 2068 bands = [] 2069 dx = (vmax - vmin) / float(n) 2070 b = [vmin, vmin + dx / 2.0, vmin + dx] 2071 i = 0 2072 while i < n: 2073 bands.append(b) 2074 b = [b[0] + dx, b[1] + dx, b[2] + dx] 2075 i += 1 2076 2077 # annotate, use the midpoint of the band as the label 2078 lut = self.mapper.GetLookupTable() 2079 labels = [] 2080 for b in bands: 2081 labels.append("{:4.2f}".format(b[1])) 2082 values = vtki.vtkVariantArray() 2083 for la in labels: 2084 values.InsertNextValue(vtki.vtkVariant(la)) 2085 for i in range(values.GetNumberOfTuples()): 2086 lut.SetAnnotation(i, values.GetValue(i).ToString()) 2087 2088 bcf = vtki.new("BandedPolyDataContourFilter") 2089 bcf.SetInputData(self.dataset) 2090 # Use either the minimum or maximum value for each band. 2091 for i, band in enumerate(bands): 2092 bcf.SetValue(i, band[2]) 2093 # We will use an indexed lookup table. 2094 bcf.SetScalarModeToIndex() 2095 bcf.GenerateContourEdgesOff() 2096 bcf.Update() 2097 bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands") 2098 2099 m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True) 2100 m1.mapper.SetLookupTable(lut) 2101 m1.mapper.SetScalarRange(lut.GetRange()) 2102 m1.pipeline = OperationNode("isobands", parents=[self]) 2103 m1.name = "IsoBands" 2104 return m1 2105 2106 def isolines(self, n=10, vmin=None, vmax=None) -> Self: 2107 """ 2108 Return a new `Mesh` representing the isolines of the active scalars. 2109 2110 Arguments: 2111 n : (int) 2112 number of isolines in the range 2113 vmin : (float) 2114 minimum of the range 2115 vmax : (float) 2116 maximum of the range 2117 2118 Examples: 2119 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2120 2121 ![](https://vedo.embl.es/images/pyplot/isolines.png) 2122 """ 2123 bcf = vtki.new("ContourFilter") 2124 bcf.SetInputData(self.dataset) 2125 r0, r1 = self.dataset.GetScalarRange() 2126 if vmin is None: 2127 vmin = r0 2128 if vmax is None: 2129 vmax = r1 2130 bcf.GenerateValues(n, vmin, vmax) 2131 bcf.Update() 2132 sf = vtki.new("Stripper") 2133 sf.SetJoinContiguousSegments(True) 2134 sf.SetInputData(bcf.GetOutput()) 2135 sf.Update() 2136 cl = vtki.new("CleanPolyData") 2137 cl.SetInputData(sf.GetOutput()) 2138 cl.Update() 2139 msh = Mesh(cl.GetOutput(), c="k").lighting("off") 2140 msh.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2141 msh.pipeline = OperationNode("isolines", parents=[self]) 2142 msh.name = "IsoLines" 2143 return msh 2144 2145 def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self: 2146 """ 2147 Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices. 2148 The input dataset is swept around the z-axis to create new polygonal primitives. 2149 For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus. 2150 2151 You can control whether the sweep of a 2D object (i.e., polygon or triangle strip) 2152 is capped with the generating geometry. 2153 Also, you can control the angle of rotation, and whether translation along the z-axis 2154 is performed along with the rotation. (Translation is useful for creating "springs"). 2155 You also can adjust the radius of the generating geometry using the "dR" keyword. 2156 2157 The skirt is generated by locating certain topological features. 2158 Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips) 2159 generate surfaces. This is true also of lines or polylines. Vertices generate lines. 2160 2161 This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses; 2162 or translational/rotational symmetric objects like springs or corkscrews. 2163 2164 Arguments: 2165 zshift : (float) 2166 shift along z axis. 2167 direction : (list) 2168 extrusion direction in the xy plane. 2169 note that zshift is forced to be the 3rd component of direction, 2170 which is therefore ignored. 2171 rotation : (float) 2172 set the angle of rotation. 2173 dr : (float) 2174 set the radius variation in absolute units. 2175 cap : (bool) 2176 enable or disable capping. 2177 res : (int) 2178 set the resolution of the generating geometry. 2179 2180 Warning: 2181 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result 2182 in two separate surfaces if capping is on, or no surface if capping is off. 2183 2184 Examples: 2185 - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py) 2186 2187 ![](https://vedo.embl.es/images/basic/extrude.png) 2188 """ 2189 rf = vtki.new("RotationalExtrusionFilter") 2190 # rf = vtki.new("LinearExtrusionFilter") 2191 rf.SetInputData(self.dataset) # must not be transformed 2192 rf.SetResolution(res) 2193 rf.SetCapping(cap) 2194 rf.SetAngle(rotation) 2195 rf.SetTranslation(zshift) 2196 rf.SetDeltaRadius(dr) 2197 rf.Update() 2198 2199 # convert triangle strips to polygonal data 2200 tris = vtki.new("TriangleFilter") 2201 tris.SetInputData(rf.GetOutput()) 2202 tris.Update() 2203 2204 m = Mesh(tris.GetOutput()) 2205 2206 if len(direction) > 1: 2207 p = self.pos() 2208 LT = vedo.LinearTransform() 2209 LT.translate(-p) 2210 LT.concatenate([ 2211 [1, 0, direction[0]], 2212 [0, 1, direction[1]], 2213 [0, 0, 1] 2214 ]) 2215 LT.translate(p) 2216 m.apply_transform(LT) 2217 2218 m.copy_properties_from(self).flat().lighting("default") 2219 m.pipeline = OperationNode( 2220 "extrude", parents=[self], 2221 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2222 ) 2223 m.name = "ExtrudedMesh" 2224 return m 2225 2226 def extrude_and_trim_with( 2227 self, 2228 surface: "Mesh", 2229 direction=(), 2230 strategy="all", 2231 cap=True, 2232 cap_strategy="max", 2233 ) -> Self: 2234 """ 2235 Extrude a Mesh and trim it with an input surface mesh. 2236 2237 Arguments: 2238 surface : (Mesh) 2239 the surface mesh to trim with. 2240 direction : (list) 2241 extrusion direction in the xy plane. 2242 strategy : (str) 2243 either "boundary_edges" or "all_edges". 2244 cap : (bool) 2245 enable or disable capping. 2246 cap_strategy : (str) 2247 either "intersection", "minimum_distance", "maximum_distance", "average_distance". 2248 2249 The input Mesh is swept along a specified direction forming a "skirt" 2250 from the boundary edges 2D primitives (i.e., edges used by only one polygon); 2251 and/or from vertices and lines. 2252 The extent of the sweeping is limited by a second input: defined where 2253 the sweep intersects a user-specified surface. 2254 2255 Capping of the extrusion can be enabled. 2256 In this case the input, generating primitive is copied inplace as well 2257 as to the end of the extrusion skirt. 2258 (See warnings below on what happens if the intersecting sweep does not 2259 intersect, or partially intersects the trim surface.) 2260 2261 Note that this method operates in two fundamentally different modes 2262 based on the extrusion strategy. 2263 If the strategy is "boundary_edges", then only the boundary edges of the input's 2264 2D primitives are extruded (verts and lines are extruded to generate lines and quads). 2265 However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives 2266 is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads). 2267 2268 Warning: 2269 The extrusion direction is assumed to define an infinite line. 2270 The intersection with the trim surface is along a ray from the - to + direction, 2271 however only the first intersection is taken. 2272 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate 2273 surfaces if capping is on and "boundary_edges" enabled, 2274 or no surface if capping is off and "boundary_edges" is enabled. 2275 If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, 2276 then no output for that primitive will be generated. In extreme cases, it is possible that no output 2277 whatsoever will be generated. 2278 2279 Example: 2280 ```python 2281 from vedo import * 2282 sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5') 2283 circle = Circle([0,0,0], r=2, res=100).color('b6') 2284 extruded_circle = circle.extrude_and_trim_with( 2285 sphere, 2286 direction=[0,-0.2,1], 2287 strategy="bound", 2288 cap=True, 2289 cap_strategy="intersection", 2290 ) 2291 circle.lw(3).color("tomato").shift(dz=-0.1) 2292 show(circle, sphere, extruded_circle, axes=1).close() 2293 ``` 2294 """ 2295 trimmer = vtki.new("TrimmedExtrusionFilter") 2296 trimmer.SetInputData(self.dataset) 2297 trimmer.SetCapping(cap) 2298 trimmer.SetExtrusionDirection(direction) 2299 trimmer.SetTrimSurfaceData(surface.dataset) 2300 if "bound" in strategy: 2301 trimmer.SetExtrusionStrategyToBoundaryEdges() 2302 elif "all" in strategy: 2303 trimmer.SetExtrusionStrategyToAllEdges() 2304 else: 2305 vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}") 2306 # print (trimmer.GetExtrusionStrategy()) 2307 2308 if "intersect" in cap_strategy: 2309 trimmer.SetCappingStrategyToIntersection() 2310 elif "min" in cap_strategy: 2311 trimmer.SetCappingStrategyToMinimumDistance() 2312 elif "max" in cap_strategy: 2313 trimmer.SetCappingStrategyToMaximumDistance() 2314 elif "ave" in cap_strategy: 2315 trimmer.SetCappingStrategyToAverageDistance() 2316 else: 2317 vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}") 2318 # print (trimmer.GetCappingStrategy()) 2319 2320 trimmer.Update() 2321 2322 m = Mesh(trimmer.GetOutput()) 2323 m.copy_properties_from(self).flat().lighting("default") 2324 m.pipeline = OperationNode( 2325 "extrude_and_trim", parents=[self, surface], 2326 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2327 ) 2328 m.name = "ExtrudedAndTrimmedMesh" 2329 return m 2330 2331 def split( 2332 self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True 2333 ) -> List[Self]: 2334 """ 2335 Split a mesh by connectivity and order the pieces by increasing area. 2336 2337 Arguments: 2338 maxdepth : (int) 2339 only consider this maximum number of mesh parts. 2340 flag : (bool) 2341 if set to True return the same single object, 2342 but add a "RegionId" array to flag the mesh subparts 2343 must_share_edge : (bool) 2344 if True, mesh regions that only share single points will be split. 2345 sort_by_area : (bool) 2346 if True, sort the mesh parts by decreasing area. 2347 2348 Examples: 2349 - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py) 2350 2351 ![](https://vedo.embl.es/images/advanced/splitmesh.png) 2352 """ 2353 pd = self.dataset 2354 if must_share_edge: 2355 if pd.GetNumberOfPolys() == 0: 2356 vedo.logger.warning("in split(): no polygons found. Skip.") 2357 return [self] 2358 cf = vtki.new("PolyDataEdgeConnectivityFilter") 2359 cf.BarrierEdgesOff() 2360 else: 2361 cf = vtki.new("PolyDataConnectivityFilter") 2362 2363 cf.SetInputData(pd) 2364 cf.SetExtractionModeToAllRegions() 2365 cf.SetColorRegions(True) 2366 cf.Update() 2367 out = cf.GetOutput() 2368 2369 if not out.GetNumberOfPoints(): 2370 return [self] 2371 2372 if flag: 2373 self.pipeline = OperationNode("split mesh", parents=[self]) 2374 self._update(out) 2375 return [self] 2376 2377 msh = Mesh(out) 2378 if must_share_edge: 2379 arr = msh.celldata["RegionId"] 2380 on = "cells" 2381 else: 2382 arr = msh.pointdata["RegionId"] 2383 on = "points" 2384 2385 alist = [] 2386 for t in range(max(arr) + 1): 2387 if t == maxdepth: 2388 break 2389 suba = msh.clone().threshold("RegionId", t, t, on=on) 2390 if sort_by_area: 2391 area = suba.area() 2392 else: 2393 area = 0 # dummy 2394 suba.name = "MeshRegion" + str(t) 2395 alist.append([suba, area]) 2396 2397 if sort_by_area: 2398 alist.sort(key=lambda x: x[1]) 2399 alist.reverse() 2400 2401 blist = [] 2402 for i, l in enumerate(alist): 2403 l[0].color(i + 1).phong() 2404 l[0].mapper.ScalarVisibilityOff() 2405 blist.append(l[0]) 2406 if i < 10: 2407 l[0].pipeline = OperationNode( 2408 f"split mesh {i}", 2409 parents=[self], 2410 comment=f"#pts {l[0].dataset.GetNumberOfPoints()}", 2411 ) 2412 return blist 2413 2414 def extract_largest_region(self) -> Self: 2415 """ 2416 Extract the largest connected part of a mesh and discard all the smaller pieces. 2417 2418 Examples: 2419 - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py) 2420 """ 2421 conn = vtki.new("PolyDataConnectivityFilter") 2422 conn.SetExtractionModeToLargestRegion() 2423 conn.ScalarConnectivityOff() 2424 conn.SetInputData(self.dataset) 2425 conn.Update() 2426 2427 m = Mesh(conn.GetOutput()) 2428 m.copy_properties_from(self) 2429 m.pipeline = OperationNode( 2430 "extract_largest_region", 2431 parents=[self], 2432 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2433 ) 2434 m.name = "MeshLargestRegion" 2435 return m 2436 2437 def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self: 2438 """Volumetric union, intersection and subtraction of surfaces. 2439 2440 Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`. 2441 2442 Two possible algorithms are available. 2443 Setting `method` to 0 (the default) uses the boolean operation algorithm 2444 written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01); 2445 setting `method` to 1 will use the "loop" boolean algorithm 2446 written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015). 2447 2448 Use `tol` to specify the absolute tolerance used to determine 2449 when the distance between two points is considered to be zero (defaults to 1e-6). 2450 2451 Example: 2452 - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py) 2453 2454 ![](https://vedo.embl.es/images/basic/boolean.png) 2455 """ 2456 if method == 0: 2457 bf = vtki.new("BooleanOperationPolyDataFilter") 2458 elif method == 1: 2459 bf = vtki.new("LoopBooleanPolyDataFilter") 2460 else: 2461 raise ValueError(f"Unknown method={method}") 2462 2463 poly1 = self.compute_normals().dataset 2464 poly2 = mesh2.compute_normals().dataset 2465 2466 if operation.lower() in ("plus", "+"): 2467 bf.SetOperationToUnion() 2468 elif operation.lower() == "intersect": 2469 bf.SetOperationToIntersection() 2470 elif operation.lower() in ("minus", "-"): 2471 bf.SetOperationToDifference() 2472 2473 if tol: 2474 bf.SetTolerance(tol) 2475 2476 bf.SetInputData(0, poly1) 2477 bf.SetInputData(1, poly2) 2478 bf.Update() 2479 2480 msh = Mesh(bf.GetOutput(), c=None) 2481 msh.flat() 2482 2483 msh.pipeline = OperationNode( 2484 "boolean " + operation, 2485 parents=[self, mesh2], 2486 shape="cylinder", 2487 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2488 ) 2489 msh.name = self.name + operation + mesh2.name 2490 return msh 2491 2492 def intersect_with(self, mesh2, tol=1e-06) -> Self: 2493 """ 2494 Intersect this Mesh with the input surface to return a set of lines. 2495 2496 Examples: 2497 - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py) 2498 2499 ![](https://vedo.embl.es/images/basic/surfIntersect.png) 2500 """ 2501 bf = vtki.new("IntersectionPolyDataFilter") 2502 bf.SetGlobalWarningDisplay(0) 2503 bf.SetTolerance(tol) 2504 bf.SetInputData(0, self.dataset) 2505 bf.SetInputData(1, mesh2.dataset) 2506 bf.Update() 2507 msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off") 2508 msh.properties.SetLineWidth(3) 2509 msh.pipeline = OperationNode( 2510 "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}" 2511 ) 2512 msh.name = "SurfaceIntersection" 2513 return msh 2514 2515 def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: 2516 """ 2517 Return the list of points intersecting the mesh 2518 along the segment defined by two points `p0` and `p1`. 2519 2520 Use `return_ids` to return the cell ids along with point coords 2521 2522 Example: 2523 ```python 2524 from vedo import * 2525 s = Spring() 2526 pts = s.intersect_with_line([0,0,0], [1,0.1,0]) 2527 ln = Line([0,0,0], [1,0.1,0], c='blue') 2528 ps = Points(pts, r=10, c='r') 2529 show(s, ln, ps, bg='white').close() 2530 ``` 2531 ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png) 2532 """ 2533 if isinstance(p0, Points): 2534 p0, p1 = p0.vertices 2535 2536 if not self.line_locator: 2537 self.line_locator = vtki.new("OBBTree") 2538 self.line_locator.SetDataSet(self.dataset) 2539 if not tol: 2540 tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000 2541 self.line_locator.SetTolerance(tol) 2542 self.line_locator.BuildLocator() 2543 2544 vpts = vtki.vtkPoints() 2545 idlist = vtki.vtkIdList() 2546 self.line_locator.IntersectWithLine(p0, p1, vpts, idlist) 2547 pts = [] 2548 for i in range(vpts.GetNumberOfPoints()): 2549 intersection: MutableSequence[float] = [0, 0, 0] 2550 vpts.GetPoint(i, intersection) 2551 pts.append(intersection) 2552 pts2 = np.array(pts) 2553 2554 if return_ids: 2555 pts_ids = [] 2556 for i in range(idlist.GetNumberOfIds()): 2557 cid = idlist.GetId(i) 2558 pts_ids.append(cid) 2559 return (pts2, np.array(pts_ids).astype(np.uint32)) 2560 2561 return pts2 2562 2563 def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 2564 """ 2565 Intersect this Mesh with a plane to return a set of lines. 2566 2567 Example: 2568 ```python 2569 from vedo import * 2570 sph = Sphere() 2571 mi = sph.clone().intersect_with_plane().join() 2572 print(mi.lines) 2573 show(sph, mi, axes=1).close() 2574 ``` 2575 ![](https://vedo.embl.es/images/feats/intersect_plane.png) 2576 """ 2577 plane = vtki.new("Plane") 2578 plane.SetOrigin(origin) 2579 plane.SetNormal(normal) 2580 2581 cutter = vtki.new("PolyDataPlaneCutter") 2582 cutter.SetInputData(self.dataset) 2583 cutter.SetPlane(plane) 2584 cutter.InterpolateAttributesOn() 2585 cutter.ComputeNormalsOff() 2586 cutter.Update() 2587 2588 msh = Mesh(cutter.GetOutput()) 2589 msh.c('k').lw(3).lighting("off") 2590 msh.pipeline = OperationNode( 2591 "intersect_with_plan", 2592 parents=[self], 2593 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2594 ) 2595 msh.name = "PlaneIntersection" 2596 return msh 2597 2598 def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]: 2599 """ 2600 Cut/clip a closed surface mesh with a collection of planes. 2601 This will produce a new closed surface by creating new polygonal 2602 faces where the input surface hits the planes. 2603 2604 The orientation of the polygons that form the surface is important. 2605 Polygons have a front face and a back face, and it's the back face that defines 2606 the interior or "solid" region of the closed surface. 2607 When a plane cuts through a "solid" region, a new cut face is generated, 2608 but not when a clipping plane cuts through a hole or "empty" region. 2609 This distinction is crucial when dealing with complex surfaces. 2610 Note that if a simple surface has its back faces pointing outwards, 2611 then that surface defines a hole in a potentially infinite solid. 2612 2613 Non-manifold surfaces should not be used with this method. 2614 2615 Arguments: 2616 origins : (list) 2617 list of plane origins 2618 normals : (list) 2619 list of plane normals 2620 invert : (bool) 2621 invert the clipping. 2622 return_assembly : (bool) 2623 return the cap and the clipped surfaces as a `vedo.Assembly`. 2624 2625 Example: 2626 ```python 2627 from vedo import * 2628 s = Sphere(res=50).linewidth(1) 2629 origins = [[-0.7, 0, 0], [0, -0.6, 0]] 2630 normals = [[-1, 0, 0], [0, -1, 0]] 2631 s.cut_closed_surface(origins, normals) 2632 show(s, axes=1).close() 2633 ``` 2634 """ 2635 planes = vtki.new("PlaneCollection") 2636 for p, s in zip(origins, normals): 2637 plane = vtki.vtkPlane() 2638 plane.SetOrigin(vedo.utils.make3d(p)) 2639 plane.SetNormal(vedo.utils.make3d(s)) 2640 planes.AddItem(plane) 2641 clipper = vtki.new("ClipClosedSurface") 2642 clipper.SetInputData(self.dataset) 2643 clipper.SetClippingPlanes(planes) 2644 clipper.PassPointDataOn() 2645 clipper.GenerateFacesOn() 2646 clipper.SetScalarModeToLabels() 2647 clipper.TriangulationErrorDisplayOn() 2648 clipper.SetInsideOut(not invert) 2649 2650 if return_assembly: 2651 clipper.GenerateClipFaceOutputOn() 2652 clipper.Update() 2653 parts = [] 2654 for i in range(clipper.GetNumberOfOutputPorts()): 2655 msh = Mesh(clipper.GetOutput(i)) 2656 msh.copy_properties_from(self) 2657 msh.name = "CutClosedSurface" 2658 msh.pipeline = OperationNode( 2659 "cut_closed_surface", 2660 parents=[self], 2661 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2662 ) 2663 parts.append(msh) 2664 asse = vedo.Assembly(parts) 2665 asse.name = "CutClosedSurface" 2666 return asse 2667 2668 else: 2669 clipper.GenerateClipFaceOutputOff() 2670 clipper.Update() 2671 self._update(clipper.GetOutput()) 2672 self.flat() 2673 self.name = "CutClosedSurface" 2674 self.pipeline = OperationNode( 2675 "cut_closed_surface", 2676 parents=[self], 2677 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 2678 ) 2679 return self 2680 2681 def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]: 2682 """ 2683 Collide this Mesh with the input surface. 2684 Information is stored in `ContactCells1` and `ContactCells2`. 2685 """ 2686 ipdf = vtki.new("CollisionDetectionFilter") 2687 # ipdf.SetGlobalWarningDisplay(0) 2688 2689 transform0 = vtki.vtkTransform() 2690 transform1 = vtki.vtkTransform() 2691 2692 # ipdf.SetBoxTolerance(tol) 2693 ipdf.SetCellTolerance(tol) 2694 ipdf.SetInputData(0, self.dataset) 2695 ipdf.SetInputData(1, mesh2.dataset) 2696 ipdf.SetTransform(0, transform0) 2697 ipdf.SetTransform(1, transform1) 2698 if return_bool: 2699 ipdf.SetCollisionModeToFirstContact() 2700 else: 2701 ipdf.SetCollisionModeToAllContacts() 2702 ipdf.Update() 2703 2704 if return_bool: 2705 return bool(ipdf.GetNumberOfContacts()) 2706 2707 msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off") 2708 msh.metadata["ContactCells1"] = vtk2numpy( 2709 ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells") 2710 ) 2711 msh.metadata["ContactCells2"] = vtk2numpy( 2712 ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells") 2713 ) 2714 msh.properties.SetLineWidth(3) 2715 2716 msh.pipeline = OperationNode( 2717 "collide_with", 2718 parents=[self, mesh2], 2719 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2720 ) 2721 msh.name = "SurfaceCollision" 2722 return msh 2723 2724 def geodesic(self, start, end) -> Self: 2725 """ 2726 Dijkstra algorithm to compute the geodesic line. 2727 Takes as input a polygonal mesh and performs a single source shortest path calculation. 2728 2729 The output mesh contains the array "VertexIDs" that contains the ordered list of vertices 2730 traversed to get from the start vertex to the end vertex. 2731 2732 Arguments: 2733 start : (int, list) 2734 start vertex index or close point `[x,y,z]` 2735 end : (int, list) 2736 end vertex index or close point `[x,y,z]` 2737 2738 Examples: 2739 - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py) 2740 2741 ![](https://vedo.embl.es/images/advanced/geodesic.png) 2742 """ 2743 if is_sequence(start): 2744 cc = self.vertices 2745 pa = Points(cc) 2746 start = pa.closest_point(start, return_point_id=True) 2747 end = pa.closest_point(end, return_point_id=True) 2748 2749 dijkstra = vtki.new("DijkstraGraphGeodesicPath") 2750 dijkstra.SetInputData(self.dataset) 2751 dijkstra.SetStartVertex(end) # inverted in vtk 2752 dijkstra.SetEndVertex(start) 2753 dijkstra.Update() 2754 2755 weights = vtki.vtkDoubleArray() 2756 dijkstra.GetCumulativeWeights(weights) 2757 2758 idlist = dijkstra.GetIdList() 2759 ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())] 2760 2761 length = weights.GetMaxId() + 1 2762 arr = np.zeros(length) 2763 for i in range(length): 2764 arr[i] = weights.GetTuple(i)[0] 2765 2766 poly = dijkstra.GetOutput() 2767 2768 vdata = numpy2vtk(arr) 2769 vdata.SetName("CumulativeWeights") 2770 poly.GetPointData().AddArray(vdata) 2771 2772 vdata2 = numpy2vtk(ids, dtype=np.uint) 2773 vdata2.SetName("VertexIDs") 2774 poly.GetPointData().AddArray(vdata2) 2775 poly.GetPointData().Modified() 2776 2777 dmesh = Mesh(poly).copy_properties_from(self) 2778 dmesh.lw(3).alpha(1).lighting("off") 2779 dmesh.name = "GeodesicLine" 2780 2781 dmesh.pipeline = OperationNode( 2782 "GeodesicLine", 2783 parents=[self], 2784 comment=f"#steps {poly.GetNumberOfPoints()}", 2785 ) 2786 return dmesh 2787 2788 ##################################################################### 2789 ### Stuff returning a Volume object 2790 ##################################################################### 2791 def binarize( 2792 self, 2793 values=(255, 0), 2794 spacing=None, 2795 dims=None, 2796 origin=None, 2797 ) -> "vedo.Volume": 2798 """ 2799 Convert a `Mesh` into a `Volume` where 2800 the interior voxels value is set to `values[0]` (255 by default), while 2801 the exterior voxels value is set to `values[1]` (0 by default). 2802 2803 Arguments: 2804 values : (list) 2805 background and foreground values. 2806 spacing : (list) 2807 voxel spacing in x, y and z. 2808 dims : (list) 2809 dimensions (nr. of voxels) of the output volume. 2810 origin : (list) 2811 position in space of the (0,0,0) voxel. 2812 2813 Examples: 2814 - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py) 2815 2816 ![](https://vedo.embl.es/images/volumetric/mesh2volume.png) 2817 """ 2818 assert len(values) == 2, "values must be a list of 2 values" 2819 fg_value, bg_value = values 2820 2821 bounds = self.bounds() 2822 if spacing is None: # compute spacing 2823 spacing = [0, 0, 0] 2824 diagonal = np.sqrt( 2825 (bounds[1] - bounds[0]) ** 2 2826 + (bounds[3] - bounds[2]) ** 2 2827 + (bounds[5] - bounds[4]) ** 2 2828 ) 2829 spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0 2830 2831 if dims is None: # compute dimensions 2832 dim = [0, 0, 0] 2833 for i in [0, 1, 2]: 2834 dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i])) 2835 else: 2836 dim = dims 2837 2838 white_img = vtki.vtkImageData() 2839 white_img.SetDimensions(dim) 2840 white_img.SetSpacing(spacing) 2841 white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1) 2842 2843 if origin is None: 2844 origin = [0, 0, 0] 2845 origin[0] = bounds[0] + spacing[0] 2846 origin[1] = bounds[2] + spacing[1] 2847 origin[2] = bounds[4] + spacing[2] 2848 white_img.SetOrigin(origin) 2849 2850 # if direction_matrix is not None: 2851 # white_img.SetDirectionMatrix(direction_matrix) 2852 2853 white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 2854 2855 # fill the image with foreground voxels: 2856 white_img.GetPointData().GetScalars().Fill(fg_value) 2857 2858 # polygonal data --> image stencil: 2859 pol2stenc = vtki.new("PolyDataToImageStencil") 2860 pol2stenc.SetInputData(self.dataset) 2861 pol2stenc.SetOutputOrigin(white_img.GetOrigin()) 2862 pol2stenc.SetOutputSpacing(white_img.GetSpacing()) 2863 pol2stenc.SetOutputWholeExtent(white_img.GetExtent()) 2864 pol2stenc.Update() 2865 2866 # cut the corresponding white image and set the background: 2867 imgstenc = vtki.new("ImageStencil") 2868 imgstenc.SetInputData(white_img) 2869 imgstenc.SetStencilConnection(pol2stenc.GetOutputPort()) 2870 # imgstenc.SetReverseStencil(True) 2871 imgstenc.SetBackgroundValue(bg_value) 2872 imgstenc.Update() 2873 2874 vol = vedo.Volume(imgstenc.GetOutput()) 2875 vol.name = "BinarizedVolume" 2876 vol.pipeline = OperationNode( 2877 "binarize", 2878 parents=[self], 2879 comment=f"dims={tuple(vol.dimensions())}", 2880 c="#e9c46a:#0096c7", 2881 ) 2882 return vol 2883 2884 def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume": 2885 """ 2886 Compute the `Volume` object whose voxels contains 2887 the signed distance from the mesh. 2888 2889 Arguments: 2890 bounds : (list) 2891 bounds of the output volume 2892 dims : (list) 2893 dimensions (nr. of voxels) of the output volume 2894 invert : (bool) 2895 flip the sign 2896 2897 Examples: 2898 - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py) 2899 """ 2900 if maxradius is not None: 2901 vedo.logger.warning( 2902 "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)." 2903 ) 2904 if bounds is None: 2905 bounds = self.bounds() 2906 sx = (bounds[1] - bounds[0]) / dims[0] 2907 sy = (bounds[3] - bounds[2]) / dims[1] 2908 sz = (bounds[5] - bounds[4]) / dims[2] 2909 2910 img = vtki.vtkImageData() 2911 img.SetDimensions(dims) 2912 img.SetSpacing(sx, sy, sz) 2913 img.SetOrigin(bounds[0], bounds[2], bounds[4]) 2914 img.AllocateScalars(vtki.VTK_FLOAT, 1) 2915 2916 imp = vtki.new("ImplicitPolyDataDistance") 2917 imp.SetInput(self.dataset) 2918 b2 = bounds[2] 2919 b4 = bounds[4] 2920 d0, d1, d2 = dims 2921 2922 for i in range(d0): 2923 x = i * sx + bounds[0] 2924 for j in range(d1): 2925 y = j * sy + b2 2926 for k in range(d2): 2927 v = imp.EvaluateFunction((x, y, k * sz + b4)) 2928 if invert: 2929 v = -v 2930 img.SetScalarComponentFromFloat(i, j, k, 0, v) 2931 2932 vol = vedo.Volume(img) 2933 vol.name = "SignedVolume" 2934 2935 vol.pipeline = OperationNode( 2936 "signed_distance", 2937 parents=[self], 2938 comment=f"dims={tuple(vol.dimensions())}", 2939 c="#e9c46a:#0096c7", 2940 ) 2941 return vol 2942 2943 def tetralize( 2944 self, 2945 side=0.02, 2946 nmax=300_000, 2947 gap=None, 2948 subsample=False, 2949 uniform=True, 2950 seed=0, 2951 debug=False, 2952 ) -> "vedo.TetMesh": 2953 """ 2954 Tetralize a closed polygonal mesh. Return a `TetMesh`. 2955 2956 Arguments: 2957 side : (float) 2958 desired side of the single tetras as fraction of the bounding box diagonal. 2959 Typical values are in the range (0.01 - 0.03) 2960 nmax : (int) 2961 maximum random numbers to be sampled in the bounding box 2962 gap : (float) 2963 keep this minimum distance from the surface, 2964 if None an automatic choice is made. 2965 subsample : (bool) 2966 subsample input surface, the geometry might be affected 2967 (the number of original faces reduceed), but higher tet quality might be obtained. 2968 uniform : (bool) 2969 generate tets more uniformly packed in the interior of the mesh 2970 seed : (int) 2971 random number generator seed 2972 debug : (bool) 2973 show an intermediate plot with sampled points 2974 2975 Examples: 2976 - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py) 2977 2978 ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg) 2979 """ 2980 surf = self.clone().clean().compute_normals() 2981 d = surf.diagonal_size() 2982 if gap is None: 2983 gap = side * d * np.sqrt(2 / 3) 2984 n = int(min((1 / side) ** 3, nmax)) 2985 2986 # fill the space w/ points 2987 x0, x1, y0, y1, z0, z1 = surf.bounds() 2988 2989 if uniform: 2990 pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42) 2991 pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100 # some small jitter 2992 else: 2993 disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2 2994 np.random.seed(seed) 2995 pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp 2996 2997 normals = surf.celldata["Normals"] 2998 cc = surf.cell_centers 2999 subpts = cc - normals * gap * 1.05 3000 pts = pts.tolist() + subpts.tolist() 3001 3002 if debug: 3003 print(".. tetralize(): subsampling and cleaning") 3004 3005 fillpts = surf.inside_points(pts) 3006 fillpts.subsample(side) 3007 3008 if gap: 3009 fillpts.distance_to(surf) 3010 fillpts.threshold("Distance", above=gap) 3011 3012 if subsample: 3013 surf.subsample(side) 3014 3015 merged_fs = vedo.merge(fillpts, surf) 3016 tmesh = merged_fs.generate_delaunay3d() 3017 tcenters = tmesh.cell_centers 3018 3019 ids = surf.inside_points(tcenters, return_ids=True) 3020 ins = np.zeros(tmesh.ncells) 3021 ins[ids] = 1 3022 3023 if debug: 3024 # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close() 3025 edges = self.edges 3026 points = self.vertices 3027 elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :]) 3028 histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d)) 3029 print(".. edges min, max", elen.min(), elen.max()) 3030 fillpts.cmap("bone") 3031 vedo.show( 3032 [ 3033 [ 3034 f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}", 3035 surf.wireframe().alpha(0.2), 3036 vedo.addons.Axes(surf), 3037 fillpts, 3038 Points(subpts).c("r4").ps(3), 3039 ], 3040 [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo], 3041 ], 3042 N=2, 3043 sharecam=False, 3044 new=True, 3045 ).close() 3046 print(".. thresholding") 3047 3048 tmesh.celldata["inside"] = ins.astype(np.uint8) 3049 tmesh.threshold("inside", above=0.9) 3050 tmesh.celldata.remove("inside") 3051 3052 if debug: 3053 print(f".. tetralize() completed, ntets = {tmesh.ncells}") 3054 3055 tmesh.pipeline = OperationNode( 3056 "tetralize", 3057 parents=[self], 3058 comment=f"#tets = {tmesh.ncells}", 3059 c="#e9c46a:#9e2a2b", 3060 ) 3061 return tmesh
Build an instance of object Mesh
derived from vedo.PointCloud
.
34 def __init__(self, inputobj=None, c="gold", alpha=1): 35 """ 36 Initialize a ``Mesh`` object. 37 38 Arguments: 39 inputobj : (str, vtkPolyData, vtkActor, vedo.Mesh) 40 If inputobj is `None` an empty mesh is created. 41 If inputobj is a `str` then it is interpreted as the name of a file to load as mesh. 42 If inputobj is an `vtkPolyData` or `vtkActor` or `vedo.Mesh` 43 then a shallow copy of it is created. 44 If inputobj is a `vedo.Mesh` then a shallow copy of it is created. 45 46 Examples: 47 - [buildmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buildmesh.py) 48 (and many others!) 49 50 ![](https://vedo.embl.es/images/basic/buildmesh.png) 51 """ 52 # print("INIT MESH", super()) 53 super().__init__() 54 55 self.name = "Mesh" 56 57 if inputobj is None: 58 # self.dataset = vtki.vtkPolyData() 59 pass 60 61 elif isinstance(inputobj, str): 62 self.dataset = vedo.file_io.load(inputobj).dataset 63 self.filename = inputobj 64 65 elif isinstance(inputobj, vtki.vtkPolyData): 66 # self.dataset.DeepCopy(inputobj) # NO 67 self.dataset = inputobj 68 if self.dataset.GetNumberOfCells() == 0: 69 carr = vtki.vtkCellArray() 70 for i in range(inputobj.GetNumberOfPoints()): 71 carr.InsertNextCell(1) 72 carr.InsertCellPoint(i) 73 self.dataset.SetVerts(carr) 74 75 elif isinstance(inputobj, Mesh): 76 self.dataset = inputobj.dataset 77 78 elif is_sequence(inputobj): 79 ninp = len(inputobj) 80 if ninp == 4: # assume input is [vertices, faces, lines, strips] 81 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2], inputobj[3]) 82 elif ninp == 3: # assume input is [vertices, faces, lines] 83 self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2]) 84 elif ninp == 2: # assume input is [vertices, faces] 85 self.dataset = buildPolyData(inputobj[0], inputobj[1]) 86 elif ninp == 1: # assume input is [vertices] 87 self.dataset = buildPolyData(inputobj[0]) 88 else: 89 vedo.logger.error("input must be a list of max 4 elements.") 90 raise ValueError() 91 92 elif isinstance(inputobj, vtki.vtkActor): 93 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 94 v = inputobj.GetMapper().GetScalarVisibility() 95 self.mapper.SetScalarVisibility(v) 96 pr = vtki.vtkProperty() 97 pr.DeepCopy(inputobj.GetProperty()) 98 self.actor.SetProperty(pr) 99 self.properties = pr 100 101 elif isinstance(inputobj, (vtki.vtkStructuredGrid, vtki.vtkRectilinearGrid)): 102 gf = vtki.new("GeometryFilter") 103 gf.SetInputData(inputobj) 104 gf.Update() 105 self.dataset = gf.GetOutput() 106 107 elif "meshlab" in str(type(inputobj)): 108 self.dataset = vedo.utils.meshlab2vedo(inputobj).dataset 109 110 elif "meshlib" in str(type(inputobj)): 111 import meshlib.mrmeshnumpy as mrmeshnumpy 112 self.dataset = buildPolyData( 113 mrmeshnumpy.getNumpyVerts(inputobj), 114 mrmeshnumpy.getNumpyFaces(inputobj.topology), 115 ) 116 117 elif "trimesh" in str(type(inputobj)): 118 self.dataset = vedo.utils.trimesh2vedo(inputobj).dataset 119 120 elif "meshio" in str(type(inputobj)): 121 # self.dataset = vedo.utils.meshio2vedo(inputobj) ##TODO 122 if len(inputobj.cells) > 0: 123 mcells = [] 124 for cellblock in inputobj.cells: 125 if cellblock.type in ("triangle", "quad"): 126 mcells += cellblock.data.tolist() 127 self.dataset = buildPolyData(inputobj.points, mcells) 128 else: 129 self.dataset = buildPolyData(inputobj.points, None) 130 # add arrays: 131 try: 132 if len(inputobj.point_data) > 0: 133 for k in inputobj.point_data.keys(): 134 vdata = numpy2vtk(inputobj.point_data[k]) 135 vdata.SetName(str(k)) 136 self.dataset.GetPointData().AddArray(vdata) 137 except AssertionError: 138 print("Could not add meshio point data, skip.") 139 140 else: 141 try: 142 gf = vtki.new("GeometryFilter") 143 gf.SetInputData(inputobj) 144 gf.Update() 145 self.dataset = gf.GetOutput() 146 except: 147 vedo.logger.error(f"cannot build mesh from type {type(inputobj)}") 148 raise RuntimeError() 149 150 self.mapper.SetInputData(self.dataset) 151 self.actor.SetMapper(self.mapper) 152 153 self.properties.SetInterpolationToPhong() 154 self.properties.SetColor(get_color(c)) 155 156 if alpha is not None: 157 self.properties.SetOpacity(alpha) 158 159 self.mapper.SetInterpolateScalarsBeforeMapping( 160 vedo.settings.interpolate_scalars_before_mapping 161 ) 162 163 if vedo.settings.use_polygon_offset: 164 self.mapper.SetResolveCoincidentTopologyToPolygonOffset() 165 pof = vedo.settings.polygon_offset_factor 166 pou = vedo.settings.polygon_offset_units 167 self.mapper.SetResolveCoincidentTopologyPolygonOffsetParameters(pof, pou) 168 169 n = self.dataset.GetNumberOfPoints() 170 self.pipeline = OperationNode(self, comment=f"#pts {n}")
Initialize a Mesh
object.
Arguments:
- inputobj : (str, vtkPolyData, vtkActor, Mesh)
If inputobj is
None
an empty mesh is created. If inputobj is astr
then it is interpreted as the name of a file to load as mesh. If inputobj is anvtkPolyData
orvtkActor
orMesh
then a shallow copy of it is created. If inputobj is aMesh
then a shallow copy of it is created.
Examples:
- buildmesh.py (and many others!)
250 def faces(self, ids=()): 251 """DEPRECATED. Use property `mesh.cells` instead.""" 252 vedo.printc("WARNING: use property mesh.cells instead of mesh.faces()",c='y') 253 return self.cells
DEPRECATED. Use property mesh.cells
instead.
255 @property 256 def edges(self): 257 """Return an array containing the edges connectivity.""" 258 extractEdges = vtki.new("ExtractEdges") 259 extractEdges.SetInputData(self.dataset) 260 # eed.UseAllPointsOn() 261 extractEdges.Update() 262 lpoly = extractEdges.GetOutput() 263 264 arr1d = vtk2numpy(lpoly.GetLines().GetData()) 265 # [nids1, id0 ... idn, niids2, id0 ... idm, etc]. 266 267 i = 0 268 conn = [] 269 n = len(arr1d) 270 for _ in range(n): 271 cell = [arr1d[i + k + 1] for k in range(arr1d[i])] 272 conn.append(cell) 273 i += arr1d[i] + 1 274 if i >= n: 275 break 276 return conn # cannot always make a numpy array of it!
Return an array containing the edges connectivity.
278 @property 279 def cell_normals(self): 280 """ 281 Retrieve face normals as a numpy array. 282 Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`. 283 """ 284 vtknormals = self.dataset.GetCellData().GetNormals() 285 numpy_normals = vtk2numpy(vtknormals) 286 if len(numpy_normals) == 0 and len(self.cells) != 0: 287 vedo.logger.warning( 288 "failed to return normal vectors.\n" 289 "You may need to call `Mesh.compute_normals()` before accessing 'Mesh.cell_normals'." 290 ) 291 numpy_normals = np.zeros((self.ncells, 3)) + [0,0,1] 292 return numpy_normals
Retrieve face normals as a numpy array.
Check out also compute_normals(cells=True)
and compute_normals_with_pca()
.
294 def compute_normals(self, points=True, cells=True, feature_angle=None, consistency=True) -> Self: 295 """ 296 Compute cell and vertex normals for the mesh. 297 298 Arguments: 299 points : (bool) 300 do the computation for the vertices too 301 cells : (bool) 302 do the computation for the cells too 303 feature_angle : (float) 304 specify the angle that defines a sharp edge. 305 If the difference in angle across neighboring polygons is greater than this value, 306 the shared edge is considered "sharp" and it is split. 307 consistency : (bool) 308 turn on/off the enforcement of consistent polygon ordering. 309 310 .. warning:: 311 If `feature_angle` is set then the Mesh can be modified, and it 312 can have a different nr. of vertices from the original. 313 314 Note that the appearance of the mesh may change if the normals are computed, 315 as shading is automatically enabled when such information is present. 316 Use `mesh.flat()` to avoid smoothing effects. 317 """ 318 pdnorm = vtki.new("PolyDataNormals") 319 pdnorm.SetInputData(self.dataset) 320 pdnorm.SetComputePointNormals(points) 321 pdnorm.SetComputeCellNormals(cells) 322 pdnorm.SetConsistency(consistency) 323 pdnorm.FlipNormalsOff() 324 if feature_angle: 325 pdnorm.SetSplitting(True) 326 pdnorm.SetFeatureAngle(feature_angle) 327 else: 328 pdnorm.SetSplitting(False) 329 pdnorm.Update() 330 out = pdnorm.GetOutput() 331 self._update(out, reset_locators=False) 332 return self
Compute cell and vertex normals for the mesh.
Arguments:
- points : (bool) do the computation for the vertices too
- cells : (bool) do the computation for the cells too
- feature_angle : (float) specify the angle that defines a sharp edge. If the difference in angle across neighboring polygons is greater than this value, the shared edge is considered "sharp" and it is split.
- consistency : (bool) turn on/off the enforcement of consistent polygon ordering.
If feature_angle
is set then the Mesh can be modified, and it
can have a different nr. of vertices from the original.
Note that the appearance of the mesh may change if the normals are computed,
as shading is automatically enabled when such information is present.
Use mesh.flat()
to avoid smoothing effects.
334 def reverse(self, cells=True, normals=False) -> Self: 335 """ 336 Reverse the order of polygonal cells 337 and/or reverse the direction of point and cell normals. 338 339 Two flags are used to control these operations: 340 - `cells=True` reverses the order of the indices in the cell connectivity list. 341 If cell is a list of IDs only those cells will be reversed. 342 - `normals=True` reverses the normals by multiplying the normal vector by -1 343 (both point and cell normals, if present). 344 """ 345 poly = self.dataset 346 347 if is_sequence(cells): 348 for cell in cells: 349 poly.ReverseCell(cell) 350 poly.GetCellData().Modified() 351 return self ############## 352 353 rev = vtki.new("ReverseSense") 354 if cells: 355 rev.ReverseCellsOn() 356 else: 357 rev.ReverseCellsOff() 358 if normals: 359 rev.ReverseNormalsOn() 360 else: 361 rev.ReverseNormalsOff() 362 rev.SetInputData(poly) 363 rev.Update() 364 self._update(rev.GetOutput(), reset_locators=False) 365 self.pipeline = OperationNode("reverse", parents=[self]) 366 return self
Reverse the order of polygonal cells and/or reverse the direction of point and cell normals.
Two flags are used to control these operations:
cells=True
reverses the order of the indices in the cell connectivity list. If cell is a list of IDs only those cells will be reversed.normals=True
reverses the normals by multiplying the normal vector by -1 (both point and cell normals, if present).
368 def volume(self) -> float: 369 """ 370 Compute the volume occupied by mesh. 371 The mesh must be triangular for this to work. 372 To triangulate a mesh use `mesh.triangulate()`. 373 """ 374 mass = vtki.new("MassProperties") 375 mass.SetGlobalWarningDisplay(0) 376 mass.SetInputData(self.dataset) 377 mass.Update() 378 mass.SetGlobalWarningDisplay(1) 379 return mass.GetVolume()
Compute the volume occupied by mesh.
The mesh must be triangular for this to work.
To triangulate a mesh use mesh.triangulate()
.
381 def area(self) -> float: 382 """ 383 Compute the surface area of the mesh. 384 The mesh must be triangular for this to work. 385 To triangulate a mesh use `mesh.triangulate()`. 386 """ 387 mass = vtki.new("MassProperties") 388 mass.SetGlobalWarningDisplay(0) 389 mass.SetInputData(self.dataset) 390 mass.Update() 391 mass.SetGlobalWarningDisplay(1) 392 return mass.GetSurfaceArea()
Compute the surface area of the mesh.
The mesh must be triangular for this to work.
To triangulate a mesh use mesh.triangulate()
.
394 def is_closed(self) -> bool: 395 """ 396 Return `True` if the mesh is watertight. 397 Note that if the mesh contains coincident points the result may be flase. 398 Use in this case `mesh.clean()` to merge coincident points. 399 """ 400 fe = vtki.new("FeatureEdges") 401 fe.BoundaryEdgesOn() 402 fe.FeatureEdgesOff() 403 fe.NonManifoldEdgesOn() 404 fe.SetInputData(self.dataset) 405 fe.Update() 406 ne = fe.GetOutput().GetNumberOfCells() 407 return not bool(ne)
Return True
if the mesh is watertight.
Note that if the mesh contains coincident points the result may be flase.
Use in this case mesh.clean()
to merge coincident points.
409 def is_manifold(self) -> bool: 410 """Return `True` if the mesh is manifold.""" 411 fe = vtki.new("FeatureEdges") 412 fe.BoundaryEdgesOff() 413 fe.FeatureEdgesOff() 414 fe.NonManifoldEdgesOn() 415 fe.SetInputData(self.dataset) 416 fe.Update() 417 ne = fe.GetOutput().GetNumberOfCells() 418 return not bool(ne)
Return True
if the mesh is manifold.
420 def non_manifold_faces(self, remove=True, tol="auto") -> Self: 421 """ 422 Detect and (try to) remove non-manifold faces of a triangular mesh: 423 424 - set `remove` to `False` to mark cells without removing them. 425 - set `tol=0` for zero-tolerance, the result will be manifold but with holes. 426 - set `tol>0` to cut off non-manifold faces, and try to recover the good ones. 427 - set `tol="auto"` to make an automatic choice of the tolerance. 428 """ 429 # mark original point and cell ids 430 self.add_ids() 431 toremove = self.boundaries( 432 boundary_edges=False, 433 non_manifold_edges=True, 434 cell_edge=True, 435 return_cell_ids=True, 436 ) 437 if len(toremove) == 0: # type: ignore 438 return self 439 440 points = self.vertices 441 faces = self.cells 442 centers = self.cell_centers 443 444 copy = self.clone() 445 copy.delete_cells(toremove).clean() 446 copy.compute_normals(cells=False) 447 normals = copy.vertex_normals 448 deltas, deltas_i = [], [] 449 450 for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"): 451 pids = copy.closest_point(centers[i], n=3, return_point_id=True) 452 norms = normals[pids] 453 n = np.mean(norms, axis=0) 454 dn = np.linalg.norm(n) 455 if not dn: 456 continue 457 n = n / dn 458 459 p0, p1, p2 = points[faces[i]][:3] 460 v = np.cross(p1 - p0, p2 - p0) 461 lv = np.linalg.norm(v) 462 if not lv: 463 continue 464 v = v / lv 465 466 cosa = 1 - np.dot(n, v) 467 deltas.append(cosa) 468 deltas_i.append(i) 469 470 recover = [] 471 if len(deltas) > 0: 472 mean_delta = np.mean(deltas) 473 err_delta = np.std(deltas) 474 txt = "" 475 if tol == "auto": # automatic choice 476 tol = mean_delta / 5 477 txt = f"\n Automatic tol. : {tol: .4f}" 478 for i, cosa in zip(deltas_i, deltas): 479 if cosa < tol: 480 recover.append(i) 481 482 vedo.logger.info( 483 f"\n --------- Non manifold faces ---------" 484 f"\n Average tol. : {mean_delta: .4f} +- {err_delta: .4f}{txt}" 485 f"\n Removed faces : {len(toremove)}" # type: ignore 486 f"\n Recovered faces: {len(recover)}" 487 ) 488 489 toremove = list(set(toremove) - set(recover)) # type: ignore 490 491 if not remove: 492 mark = np.zeros(self.ncells, dtype=np.uint8) 493 mark[recover] = 1 494 mark[toremove] = 2 495 self.celldata["NonManifoldCell"] = mark 496 else: 497 self.delete_cells(toremove) # type: ignore 498 499 self.pipeline = OperationNode( 500 "non_manifold_faces", 501 parents=[self], 502 comment=f"#cells {self.dataset.GetNumberOfCells()}", 503 ) 504 return self
Detect and (try to) remove non-manifold faces of a triangular mesh:
- set `remove` to `False` to mark cells without removing them.
- set `tol=0` for zero-tolerance, the result will be manifold but with holes.
- set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
- set `tol="auto"` to make an automatic choice of the tolerance.
507 def euler_characteristic(self) -> int: 508 """ 509 Compute the Euler characteristic of the mesh. 510 The Euler characteristic is a topological invariant for surfaces. 511 """ 512 return self.npoints - len(self.edges) + self.ncells
Compute the Euler characteristic of the mesh. The Euler characteristic is a topological invariant for surfaces.
514 def genus(self) -> int: 515 """ 516 Compute the genus of the mesh. 517 The genus is a topological invariant for surfaces. 518 """ 519 nb = len(self.boundaries().split()) - 1 520 return (2 - self.euler_characteristic() - nb ) / 2
Compute the genus of the mesh. The genus is a topological invariant for surfaces.
522 def to_reeb_graph(self, field_id=0): 523 """ 524 Convert the mesh into a Reeb graph. 525 The Reeb graph is a topological structure that captures the evolution 526 of the level sets of a scalar field. 527 528 Arguments: 529 field_id : (int) 530 the id of the scalar field to use. 531 532 Example: 533 ```python 534 from vedo import * 535 mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl") 536 mesh.rotate_x(10).rotate_y(15).alpha(0.5) 537 mesh.pointdata["scalars"] = mesh.vertices[:, 2] 538 539 printc("is_closed :", mesh.is_closed()) 540 printc("is_manifold:", mesh.is_manifold()) 541 printc("euler_char :", mesh.euler_characteristic()) 542 printc("genus :", mesh.genus()) 543 544 reeb = mesh.to_reeb_graph() 545 ids = reeb[0].pointdata["Vertex Ids"] 546 pts = Points(mesh.vertices[ids], r=10) 547 548 show([[mesh, pts], reeb], N=2, sharecam=False) 549 ``` 550 """ 551 rg = vtki.new("PolyDataToReebGraphFilter") 552 rg.SetInputData(self.dataset) 553 rg.SetFieldId(field_id) 554 rg.Update() 555 gr = vedo.pyplot.DirectedGraph() 556 gr.mdg = rg.GetOutput() 557 gr.build() 558 return gr
Convert the mesh into a Reeb graph. The Reeb graph is a topological structure that captures the evolution of the level sets of a scalar field.
Arguments:
- field_id : (int) the id of the scalar field to use.
Example:
from vedo import * mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl") mesh.rotate_x(10).rotate_y(15).alpha(0.5) mesh.pointdata["scalars"] = mesh.vertices[:, 2] printc("is_closed :", mesh.is_closed()) printc("is_manifold:", mesh.is_manifold()) printc("euler_char :", mesh.euler_characteristic()) printc("genus :", mesh.genus()) reeb = mesh.to_reeb_graph() ids = reeb[0].pointdata["Vertex Ids"] pts = Points(mesh.vertices[ids], r=10) show([[mesh, pts], reeb], N=2, sharecam=False)
561 def shrink(self, fraction=0.85) -> Self: 562 """ 563 Shrink the triangle polydata in the representation of the input mesh. 564 565 Examples: 566 - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py) 567 568 ![](https://vedo.embl.es/images/basic/shrink.png) 569 """ 570 # Overriding base class method core.shrink() 571 shrink = vtki.new("ShrinkPolyData") 572 shrink.SetInputData(self.dataset) 573 shrink.SetShrinkFactor(fraction) 574 shrink.Update() 575 self._update(shrink.GetOutput()) 576 self.pipeline = OperationNode("shrink", parents=[self]) 577 return self
579 def cap(self, return_cap=False) -> Self: 580 """ 581 Generate a "cap" on a clipped mesh, or caps sharp edges. 582 583 Examples: 584 - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py) 585 586 ![](https://vedo.embl.es/images/advanced/cutAndCap.png) 587 588 See also: `join()`, `join_segments()`, `slice()`. 589 """ 590 fe = vtki.new("FeatureEdges") 591 fe.SetInputData(self.dataset) 592 fe.BoundaryEdgesOn() 593 fe.FeatureEdgesOff() 594 fe.NonManifoldEdgesOff() 595 fe.ManifoldEdgesOff() 596 fe.Update() 597 598 stripper = vtki.new("Stripper") 599 stripper.SetInputData(fe.GetOutput()) 600 stripper.JoinContiguousSegmentsOn() 601 stripper.Update() 602 603 boundary_poly = vtki.vtkPolyData() 604 boundary_poly.SetPoints(stripper.GetOutput().GetPoints()) 605 boundary_poly.SetPolys(stripper.GetOutput().GetLines()) 606 607 rev = vtki.new("ReverseSense") 608 rev.ReverseCellsOn() 609 rev.SetInputData(boundary_poly) 610 rev.Update() 611 612 tf = vtki.new("TriangleFilter") 613 tf.SetInputData(rev.GetOutput()) 614 tf.Update() 615 616 if return_cap: 617 m = Mesh(tf.GetOutput()) 618 m.pipeline = OperationNode( 619 "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}" 620 ) 621 m.name = "MeshCap" 622 return m 623 624 polyapp = vtki.new("AppendPolyData") 625 polyapp.AddInputData(self.dataset) 626 polyapp.AddInputData(tf.GetOutput()) 627 polyapp.Update() 628 629 self._update(polyapp.GetOutput()) 630 self.clean() 631 632 self.pipeline = OperationNode( 633 "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 634 ) 635 return self
Generate a "cap" on a clipped mesh, or caps sharp edges.
Examples:
See also: join()
, join_segments()
, slice()
.
637 def join(self, polys=True, reset=False) -> Self: 638 """ 639 Generate triangle strips and/or polylines from 640 input polygons, triangle strips, and lines. 641 642 Input polygons are assembled into triangle strips only if they are triangles; 643 other types of polygons are passed through to the output and not stripped. 644 Use mesh.triangulate() to triangulate non-triangular polygons prior to running 645 this filter if you need to strip all the data. 646 647 Also note that if triangle strips or polylines are present in the input 648 they are passed through and not joined nor extended. 649 If you wish to strip these use mesh.triangulate() to fragment the input 650 into triangles and lines prior to applying join(). 651 652 Arguments: 653 polys : (bool) 654 polygonal segments will be joined if they are contiguous 655 reset : (bool) 656 reset points ordering 657 658 Warning: 659 If triangle strips or polylines exist in the input data 660 they will be passed through to the output data. 661 This filter will only construct triangle strips if triangle polygons 662 are available; and will only construct polylines if lines are available. 663 664 Example: 665 ```python 666 from vedo import * 667 c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate() 668 c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate() 669 intersect = c1.intersect_with(c2).join(reset=True) 670 spline = Spline(intersect).c('blue').lw(5) 671 show(c1, c2, spline, intersect.labels('id'), axes=1).close() 672 ``` 673 ![](https://vedo.embl.es/images/feats/line_join.png) 674 """ 675 sf = vtki.new("Stripper") 676 sf.SetPassThroughCellIds(True) 677 sf.SetPassThroughPointIds(True) 678 sf.SetJoinContiguousSegments(polys) 679 sf.SetInputData(self.dataset) 680 sf.Update() 681 if reset: 682 poly = sf.GetOutput() 683 cpd = vtki.new("CleanPolyData") 684 cpd.PointMergingOn() 685 cpd.ConvertLinesToPointsOn() 686 cpd.ConvertPolysToLinesOn() 687 cpd.ConvertStripsToPolysOn() 688 cpd.SetInputData(poly) 689 cpd.Update() 690 poly = cpd.GetOutput() 691 vpts = poly.GetCell(0).GetPoints().GetData() 692 poly.GetPoints().SetData(vpts) 693 else: 694 poly = sf.GetOutput() 695 696 self._update(poly) 697 698 self.pipeline = OperationNode( 699 "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 700 ) 701 return self
Generate triangle strips and/or polylines from input polygons, triangle strips, and lines.
Input polygons are assembled into triangle strips only if they are triangles; other types of polygons are passed through to the output and not stripped. Use mesh.triangulate() to triangulate non-triangular polygons prior to running this filter if you need to strip all the data.
Also note that if triangle strips or polylines are present in the input they are passed through and not joined nor extended. If you wish to strip these use mesh.triangulate() to fragment the input into triangles and lines prior to applying join().
Arguments:
- polys : (bool) polygonal segments will be joined if they are contiguous
- reset : (bool) reset points ordering
Warning:
If triangle strips or polylines exist in the input data they will be passed through to the output data. This filter will only construct triangle strips if triangle polygons are available; and will only construct polylines if lines are available.
Example:
from vedo import * c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate() c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate() intersect = c1.intersect_with(c2).join(reset=True) spline = Spline(intersect).c('blue').lw(5) show(c1, c2, spline, intersect.labels('id'), axes=1).close()
703 def join_segments(self, closed=True, tol=1e-03) -> list: 704 """ 705 Join line segments into contiguous lines. 706 Useful to call with `triangulate()` method. 707 708 Returns: 709 list of `shapes.Lines` 710 711 Example: 712 ```python 713 from vedo import * 714 msh = Torus().alpha(0.1).wireframe() 715 intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5') 716 slices = [s.triangulate() for s in intersection.join_segments()] 717 show(msh, intersection, merge(slices), axes=1, viewup='z') 718 ``` 719 ![](https://vedo.embl.es/images/feats/join_segments.jpg) 720 """ 721 vlines = [] 722 for ipiece, outline in enumerate(self.split(must_share_edge=False)): # type: ignore 723 724 outline.clean() 725 pts = outline.vertices 726 if len(pts) < 3: 727 continue 728 avesize = outline.average_size() 729 lines = outline.lines 730 # print("---lines", lines, "in piece", ipiece) 731 tol = avesize / pts.shape[0] * tol 732 733 k = 0 734 joinedpts = [pts[k]] 735 for _ in range(len(pts)): 736 pk = pts[k] 737 for j, line in enumerate(lines): 738 739 id0, id1 = line[0], line[-1] 740 p0, p1 = pts[id0], pts[id1] 741 742 if np.linalg.norm(p0 - pk) < tol: 743 n = len(line) 744 for m in range(1, n): 745 joinedpts.append(pts[line[m]]) 746 # joinedpts.append(p1) 747 k = id1 748 lines.pop(j) 749 break 750 751 elif np.linalg.norm(p1 - pk) < tol: 752 n = len(line) 753 for m in reversed(range(0, n - 1)): 754 joinedpts.append(pts[line[m]]) 755 # joinedpts.append(p0) 756 k = id0 757 lines.pop(j) 758 break 759 760 if len(joinedpts) > 1: 761 newline = vedo.shapes.Line(joinedpts, closed=closed) 762 newline.clean() 763 newline.actor.SetProperty(self.properties) 764 newline.properties = self.properties 765 newline.pipeline = OperationNode( 766 "join_segments", 767 parents=[self], 768 comment=f"#pts {newline.dataset.GetNumberOfPoints()}", 769 ) 770 vlines.append(newline) 771 772 return vlines
Join line segments into contiguous lines.
Useful to call with triangulate()
method.
Returns:
list of
shapes.Lines
Example:
from vedo import * msh = Torus().alpha(0.1).wireframe() intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5') slices = [s.triangulate() for s in intersection.join_segments()] show(msh, intersection, merge(slices), axes=1, viewup='z')
774 def join_with_strips(self, b1, closed=True) -> Self: 775 """ 776 Join booundary lines by creating a triangle strip between them. 777 778 Example: 779 ```python 780 from vedo import * 781 m1 = Cylinder(cap=False).boundaries() 782 m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1) 783 strips = m1.join_with_strips(m2) 784 show(m1, m2, strips, axes=1).close() 785 ``` 786 """ 787 b0 = self.clone().join() 788 b1 = b1.clone().join() 789 790 vertices0 = b0.vertices.tolist() 791 vertices1 = b1.vertices.tolist() 792 793 lines0 = b0.lines 794 lines1 = b1.lines 795 m = len(lines0) 796 assert m == len(lines1), ( 797 "lines must have the same number of points\n" 798 f"line has {m} points in b0 and {len(lines1)} in b1" 799 ) 800 801 strips = [] 802 points: List[Any] = [] 803 804 for j in range(m): 805 806 ids0j = list(lines0[j]) 807 ids1j = list(lines1[j]) 808 809 n = len(ids0j) 810 assert n == len(ids1j), ( 811 "lines must have the same number of points\n" 812 f"line {j} has {n} points in b0 and {len(ids1j)} in b1" 813 ) 814 815 if closed: 816 ids0j.append(ids0j[0]) 817 ids1j.append(ids1j[0]) 818 vertices0.append(vertices0[ids0j[0]]) 819 vertices1.append(vertices1[ids1j[0]]) 820 n = n + 1 821 822 strip = [] # create a triangle strip 823 npt = len(points) 824 for ipt in range(n): 825 points.append(vertices0[ids0j[ipt]]) 826 points.append(vertices1[ids1j[ipt]]) 827 828 strip = list(range(npt, npt + 2*n)) 829 strips.append(strip) 830 831 return Mesh([points, [], [], strips], c="k6")
Join booundary lines by creating a triangle strip between them.
Example:
from vedo import *
m1 = Cylinder(cap=False).boundaries()
m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
strips = m1.join_with_strips(m2)
show(m1, m2, strips, axes=1).close()
833 def split_polylines(self) -> Self: 834 """Split polylines into separate segments.""" 835 tf = vtki.new("TriangleFilter") 836 tf.SetPassLines(True) 837 tf.SetPassVerts(False) 838 tf.SetInputData(self.dataset) 839 tf.Update() 840 self._update(tf.GetOutput(), reset_locators=False) 841 self.lw(0).lighting("default").pickable() 842 self.pipeline = OperationNode( 843 "split_polylines", parents=[self], 844 comment=f"#lines {self.dataset.GetNumberOfLines()}" 845 ) 846 return self
Split polylines into separate segments.
848 def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 849 """ 850 Slice a mesh with a plane and fill the contour. 851 852 Example: 853 ```python 854 from vedo import * 855 msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe() 856 mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0]) 857 mslice.c('purple5') 858 show(msh, mslice, axes=1) 859 ``` 860 ![](https://vedo.embl.es/images/feats/mesh_slice.jpg) 861 862 See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`. 863 """ 864 intersection = self.intersect_with_plane(origin=origin, normal=normal) 865 slices = [s.triangulate() for s in intersection.join_segments()] 866 mslices = vedo.pointcloud.merge(slices) 867 if mslices: 868 mslices.name = "MeshSlice" 869 mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}") 870 return mslices
Slice a mesh with a plane and fill the contour.
Example:
from vedo import * msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe() mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0]) mslice.c('purple5') show(msh, mslice, axes=1)
See also: join()
, join_segments()
, cap()
, cut_with_plane()
.
872 def triangulate(self, verts=True, lines=True) -> Self: 873 """ 874 Converts mesh polygons into triangles. 875 876 If the input mesh is only made of 2D lines (no faces) the output will be a triangulation 877 that fills the internal area. The contours may be concave, and may even contain holes, 878 i.e. a contour may contain an internal contour winding in the opposite 879 direction to indicate that it is a hole. 880 881 Arguments: 882 verts : (bool) 883 if True, break input vertex cells into individual vertex cells (one point per cell). 884 If False, the input vertex cells will be ignored. 885 lines : (bool) 886 if True, break input polylines into line segments. 887 If False, input lines will be ignored and the output will have no lines. 888 """ 889 if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips(): 890 # print("Using vtkTriangleFilter") 891 tf = vtki.new("TriangleFilter") 892 tf.SetPassLines(lines) 893 tf.SetPassVerts(verts) 894 895 elif self.dataset.GetNumberOfLines(): 896 # print("Using vtkContourTriangulator") 897 tf = vtki.new("ContourTriangulator") 898 tf.TriangulationErrorDisplayOn() 899 900 else: 901 vedo.logger.debug("input in triangulate() seems to be void! Skip.") 902 return self 903 904 tf.SetInputData(self.dataset) 905 tf.Update() 906 self._update(tf.GetOutput(), reset_locators=False) 907 self.lw(0).lighting("default").pickable() 908 909 self.pipeline = OperationNode( 910 "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}" 911 ) 912 return self
Converts mesh polygons into triangles.
If the input mesh is only made of 2D lines (no faces) the output will be a triangulation that fills the internal area. The contours may be concave, and may even contain holes, i.e. a contour may contain an internal contour winding in the opposite direction to indicate that it is a hole.
Arguments:
- verts : (bool) if True, break input vertex cells into individual vertex cells (one point per cell). If False, the input vertex cells will be ignored.
- lines : (bool) if True, break input polylines into line segments. If False, input lines will be ignored and the output will have no lines.
914 def compute_cell_vertex_count(self) -> Self: 915 """ 916 Add to this mesh a cell data array containing the nr of vertices that a polygonal face has. 917 """ 918 csf = vtki.new("CellSizeFilter") 919 csf.SetInputData(self.dataset) 920 csf.SetComputeArea(False) 921 csf.SetComputeVolume(False) 922 csf.SetComputeLength(False) 923 csf.SetComputeVertexCount(True) 924 csf.SetVertexCountArrayName("VertexCount") 925 csf.Update() 926 self.dataset.GetCellData().AddArray( 927 csf.GetOutput().GetCellData().GetArray("VertexCount") 928 ) 929 return self
Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.
931 def compute_quality(self, metric=6) -> Self: 932 """ 933 Calculate metrics of quality for the elements of a triangular mesh. 934 This method adds to the mesh a cell array named "Quality". 935 See class 936 [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html). 937 938 Arguments: 939 metric : (int) 940 type of available estimators are: 941 - EDGE RATIO, 0 942 - ASPECT RATIO, 1 943 - RADIUS RATIO, 2 944 - ASPECT FROBENIUS, 3 945 - MED ASPECT FROBENIUS, 4 946 - MAX ASPECT FROBENIUS, 5 947 - MIN_ANGLE, 6 948 - COLLAPSE RATIO, 7 949 - MAX ANGLE, 8 950 - CONDITION, 9 951 - SCALED JACOBIAN, 10 952 - SHEAR, 11 953 - RELATIVE SIZE SQUARED, 12 954 - SHAPE, 13 955 - SHAPE AND SIZE, 14 956 - DISTORTION, 15 957 - MAX EDGE RATIO, 16 958 - SKEW, 17 959 - TAPER, 18 960 - VOLUME, 19 961 - STRETCH, 20 962 - DIAGONAL, 21 963 - DIMENSION, 22 964 - ODDY, 23 965 - SHEAR AND SIZE, 24 966 - JACOBIAN, 25 967 - WARPAGE, 26 968 - ASPECT GAMMA, 27 969 - AREA, 28 970 - ASPECT BETA, 29 971 972 Examples: 973 - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py) 974 975 ![](https://vedo.embl.es/images/advanced/meshquality.png) 976 """ 977 qf = vtki.new("MeshQuality") 978 qf.SetInputData(self.dataset) 979 qf.SetTriangleQualityMeasure(metric) 980 qf.SaveCellQualityOn() 981 qf.Update() 982 self._update(qf.GetOutput(), reset_locators=False) 983 self.mapper.SetScalarModeToUseCellData() 984 self.pipeline = OperationNode("compute_quality", parents=[self]) 985 return self
Calculate metrics of quality for the elements of a triangular mesh. This method adds to the mesh a cell array named "Quality". See class vtkMeshQuality.
Arguments:
- metric : (int)
type of available estimators are:
- EDGE RATIO, 0
- ASPECT RATIO, 1
- RADIUS RATIO, 2
- ASPECT FROBENIUS, 3
- MED ASPECT FROBENIUS, 4
- MAX ASPECT FROBENIUS, 5
- MIN_ANGLE, 6
- COLLAPSE RATIO, 7
- MAX ANGLE, 8
- CONDITION, 9
- SCALED JACOBIAN, 10
- SHEAR, 11
- RELATIVE SIZE SQUARED, 12
- SHAPE, 13
- SHAPE AND SIZE, 14
- DISTORTION, 15
- MAX EDGE RATIO, 16
- SKEW, 17
- TAPER, 18
- VOLUME, 19
- STRETCH, 20
- DIAGONAL, 21
- DIMENSION, 22
- ODDY, 23
- SHEAR AND SIZE, 24
- JACOBIAN, 25
- WARPAGE, 26
- ASPECT GAMMA, 27
- AREA, 28
- ASPECT BETA, 29
Examples:
987 def count_vertices(self) -> np.ndarray: 988 """Count the number of vertices each cell has and return it as a numpy array""" 989 vc = vtki.new("CountVertices") 990 vc.SetInputData(self.dataset) 991 vc.SetOutputArrayName("VertexCount") 992 vc.Update() 993 varr = vc.GetOutput().GetCellData().GetArray("VertexCount") 994 return vtk2numpy(varr)
Count the number of vertices each cell has and return it as a numpy array
996 def check_validity(self, tol=0) -> np.ndarray: 997 """ 998 Return a numpy array of possible problematic faces following this convention: 999 - Valid = 0 1000 - WrongNumberOfPoints = 1 1001 - IntersectingEdges = 2 1002 - IntersectingFaces = 4 1003 - NoncontiguousEdges = 8 1004 - Nonconvex = 10 1005 - OrientedIncorrectly = 20 1006 1007 Arguments: 1008 tol : (float) 1009 value is used as an epsilon for floating point 1010 equality checks throughout the cell checking process. 1011 """ 1012 vald = vtki.new("CellValidator") 1013 if tol: 1014 vald.SetTolerance(tol) 1015 vald.SetInputData(self.dataset) 1016 vald.Update() 1017 varr = vald.GetOutput().GetCellData().GetArray("ValidityState") 1018 return vtk2numpy(varr)
Return a numpy array of possible problematic faces following this convention:
- Valid = 0
- WrongNumberOfPoints = 1
- IntersectingEdges = 2
- IntersectingFaces = 4
- NoncontiguousEdges = 8
- Nonconvex = 10
- OrientedIncorrectly = 20
Arguments:
- tol : (float) value is used as an epsilon for floating point equality checks throughout the cell checking process.
1020 def compute_curvature(self, method=0) -> Self: 1021 """ 1022 Add scalars to `Mesh` that contains the curvature calculated in three different ways. 1023 1024 Variable `method` can be: 1025 - 0 = gaussian 1026 - 1 = mean curvature 1027 - 2 = max curvature 1028 - 3 = min curvature 1029 1030 Example: 1031 ```python 1032 from vedo import Torus 1033 Torus().compute_curvature().add_scalarbar().show().close() 1034 ``` 1035 ![](https://vedo.embl.es/images/advanced/torus_curv.png) 1036 """ 1037 curve = vtki.new("Curvatures") 1038 curve.SetInputData(self.dataset) 1039 curve.SetCurvatureType(method) 1040 curve.Update() 1041 self._update(curve.GetOutput(), reset_locators=False) 1042 self.mapper.ScalarVisibilityOn() 1043 return self
Add scalars to Mesh
that contains the curvature calculated in three different ways.
Variable method
can be:
- 0 = gaussian
- 1 = mean curvature
- 2 = max curvature
- 3 = min curvature
Example:
from vedo import Torus Torus().compute_curvature().add_scalarbar().show().close()
1045 def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self: 1046 """ 1047 Add to `Mesh` a scalar array that contains distance along a specified direction. 1048 1049 Arguments: 1050 low : (list) 1051 one end of the line (small scalar values) 1052 high : (list) 1053 other end of the line (large scalar values) 1054 vrange : (list) 1055 set the range of the scalar 1056 1057 Example: 1058 ```python 1059 from vedo import Sphere 1060 s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1)) 1061 s.add_scalarbar().show(axes=1).close() 1062 ``` 1063 ![](https://vedo.embl.es/images/basic/compute_elevation.png) 1064 """ 1065 ef = vtki.new("ElevationFilter") 1066 ef.SetInputData(self.dataset) 1067 ef.SetLowPoint(low) 1068 ef.SetHighPoint(high) 1069 ef.SetScalarRange(vrange) 1070 ef.Update() 1071 self._update(ef.GetOutput(), reset_locators=False) 1072 self.mapper.ScalarVisibilityOn() 1073 return self
Add to Mesh
a scalar array that contains distance along a specified direction.
Arguments:
- low : (list) one end of the line (small scalar values)
- high : (list) other end of the line (large scalar values)
- vrange : (list) set the range of the scalar
Example:
from vedo import Sphere s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1)) s.add_scalarbar().show(axes=1).close()
1076 def laplacian_diffusion(self, array_name, dt, num_steps) -> Self: 1077 """ 1078 Apply a diffusion process to a scalar array defined on the points of a mesh. 1079 1080 Arguments: 1081 array_name : (str) 1082 name of the array to diffuse. 1083 dt : (float) 1084 time step. 1085 num_steps : (int) 1086 number of iterations. 1087 """ 1088 try: 1089 import scipy.sparse 1090 import scipy.sparse.linalg 1091 except ImportError: 1092 vedo.logger.error("scipy not found. Cannot run laplacian_diffusion()") 1093 return self 1094 1095 def build_laplacian(): 1096 rows = [] 1097 cols = [] 1098 data = [] 1099 n_points = points.shape[0] 1100 avg_area = np.mean(areas) * 10000 1101 # print("avg_area", avg_area) 1102 1103 for triangle in cells: 1104 for i in range(3): 1105 for j in range(i + 1, 3): 1106 u = triangle[i] 1107 v = triangle[j] 1108 rows.append(u) 1109 cols.append(v) 1110 rows.append(v) 1111 cols.append(u) 1112 data.append(-1/avg_area) 1113 data.append(-1/avg_area) 1114 1115 L = scipy.sparse.coo_matrix( 1116 (data, (rows, cols)), shape=(n_points, n_points) 1117 ).tocsc() 1118 1119 degree = -np.array(L.sum(axis=1)).flatten() # adjust the diagonal 1120 # print("degree", degree) 1121 L.setdiag(degree) 1122 return L 1123 1124 def _diffuse(u0, L, dt, num_steps): 1125 # mean_area = np.mean(areas) * 10000 1126 # print("mean_area", mean_area) 1127 mean_area = 1 1128 I = scipy.sparse.eye(L.shape[0], format="csc") 1129 A = I - (dt/mean_area) * L 1130 u = u0 1131 for _ in range(int(num_steps)): 1132 u = A.dot(u) 1133 return u 1134 1135 self.compute_cell_size() 1136 areas = self.celldata["Area"] 1137 points = self.vertices 1138 cells = self.cells 1139 u0 = self.pointdata[array_name] 1140 1141 # Simulate diffusion 1142 L = build_laplacian() 1143 u = _diffuse(u0, L, dt, num_steps) 1144 self.pointdata[array_name] = u 1145 return self
Apply a diffusion process to a scalar array defined on the points of a mesh.
Arguments:
- array_name : (str) name of the array to diffuse.
- dt : (float) time step.
- num_steps : (int) number of iterations.
1148 def subdivide(self, n=1, method=0, mel=None) -> Self: 1149 """ 1150 Increase the number of vertices of a surface mesh. 1151 1152 Arguments: 1153 n : (int) 1154 number of subdivisions. 1155 method : (int) 1156 Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4) 1157 mel : (float) 1158 Maximum Edge Length (applicable to Adaptive method only). 1159 """ 1160 triangles = vtki.new("TriangleFilter") 1161 triangles.SetInputData(self.dataset) 1162 triangles.Update() 1163 tri_mesh = triangles.GetOutput() 1164 if method == 0: 1165 sdf = vtki.new("LoopSubdivisionFilter") 1166 elif method == 1: 1167 sdf = vtki.new("LinearSubdivisionFilter") 1168 elif method == 2: 1169 sdf = vtki.new("AdaptiveSubdivisionFilter") 1170 if mel is None: 1171 mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n 1172 sdf.SetMaximumEdgeLength(mel) 1173 elif method == 3: 1174 sdf = vtki.new("ButterflySubdivisionFilter") 1175 elif method == 4: 1176 sdf = vtki.new("DensifyPolyData") 1177 else: 1178 vedo.logger.error(f"in subdivide() unknown method {method}") 1179 raise RuntimeError() 1180 1181 if method != 2: 1182 sdf.SetNumberOfSubdivisions(n) 1183 1184 sdf.SetInputData(tri_mesh) 1185 sdf.Update() 1186 1187 self._update(sdf.GetOutput()) 1188 1189 self.pipeline = OperationNode( 1190 "subdivide", 1191 parents=[self], 1192 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1193 ) 1194 return self
Increase the number of vertices of a surface mesh.
Arguments:
- n : (int) number of subdivisions.
- method : (int) Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
- mel : (float) Maximum Edge Length (applicable to Adaptive method only).
1197 def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self: 1198 """ 1199 Downsample the number of vertices in a mesh to `fraction`. 1200 1201 This filter preserves the `pointdata` of the input dataset. In previous versions 1202 of vedo, this decimation algorithm was referred to as quadric decimation. 1203 1204 Arguments: 1205 fraction : (float) 1206 the desired target of reduction. 1207 n : (int) 1208 the desired number of final points 1209 (`fraction` is recalculated based on it). 1210 preserve_volume : (bool) 1211 Decide whether to activate volume preservation which greatly 1212 reduces errors in triangle normal direction. 1213 regularization : (float) 1214 regularize the point finding algorithm so as to have better quality 1215 mesh elements at the cost of a slightly lower precision on the 1216 geometry potentially (mostly at sharp edges). 1217 Can be useful for decimating meshes that have been triangulated on noisy data. 1218 1219 Note: 1220 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1221 Internally the VTK class 1222 [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html) 1223 is used for this operation. 1224 1225 See also: `decimate_binned()` and `decimate_pro()`. 1226 """ 1227 poly = self.dataset 1228 if n: # N = desired number of points 1229 npt = poly.GetNumberOfPoints() 1230 fraction = n / npt 1231 if fraction >= 1: 1232 return self 1233 1234 decimate = vtki.new("QuadricDecimation") 1235 decimate.SetVolumePreservation(preserve_volume) 1236 # decimate.AttributeErrorMetricOn() 1237 if regularization: 1238 decimate.SetRegularize(True) 1239 decimate.SetRegularization(regularization) 1240 1241 try: 1242 decimate.MapPointDataOn() 1243 except AttributeError: 1244 pass 1245 1246 decimate.SetTargetReduction(1 - fraction) 1247 decimate.SetInputData(poly) 1248 decimate.Update() 1249 1250 self._update(decimate.GetOutput()) 1251 self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction() 1252 1253 self.pipeline = OperationNode( 1254 "decimate", 1255 parents=[self], 1256 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1257 ) 1258 return self
Downsample the number of vertices in a mesh to fraction
.
This filter preserves the pointdata
of the input dataset. In previous versions
of vedo, this decimation algorithm was referred to as quadric decimation.
Arguments:
- fraction : (float) the desired target of reduction.
- n : (int)
the desired number of final points
(
fraction
is recalculated based on it). - preserve_volume : (bool) Decide whether to activate volume preservation which greatly reduces errors in triangle normal direction.
- regularization : (float) regularize the point finding algorithm so as to have better quality mesh elements at the cost of a slightly lower precision on the geometry potentially (mostly at sharp edges). Can be useful for decimating meshes that have been triangulated on noisy data.
Note:
Setting
fraction=0.1
leaves 10% of the original number of vertices. Internally the VTK class vtkQuadricDecimation is used for this operation.
See also: decimate_binned()
and decimate_pro()
.
1260 def decimate_pro( 1261 self, 1262 fraction=0.5, 1263 n=None, 1264 preserve_topology=True, 1265 preserve_boundaries=True, 1266 splitting=False, 1267 splitting_angle=75, 1268 feature_angle=0, 1269 inflection_point_ratio=10, 1270 vertex_degree=0, 1271 ) -> Self: 1272 """ 1273 Downsample the number of vertices in a mesh to `fraction`. 1274 1275 This filter preserves the `pointdata` of the input dataset. 1276 1277 Arguments: 1278 fraction : (float) 1279 The desired target of reduction. 1280 Setting `fraction=0.1` leaves 10% of the original number of vertices. 1281 n : (int) 1282 the desired number of final points (`fraction` is recalculated based on it). 1283 preserve_topology : (bool) 1284 If on, mesh splitting and hole elimination will not occur. 1285 This may limit the maximum reduction that may be achieved. 1286 preserve_boundaries : (bool) 1287 Turn on/off the deletion of vertices on the boundary of a mesh. 1288 Control whether mesh boundaries are preserved during decimation. 1289 feature_angle : (float) 1290 Specify the angle that defines a feature. 1291 This angle is used to define what an edge is 1292 (i.e., if the surface normal between two adjacent triangles 1293 is >= FeatureAngle, an edge exists). 1294 splitting : (bool) 1295 Turn on/off the splitting of the mesh at corners, 1296 along edges, at non-manifold points, or anywhere else a split is required. 1297 Turning splitting off will better preserve the original topology of the mesh, 1298 but you may not obtain the requested reduction. 1299 splitting_angle : (float) 1300 Specify the angle that defines a sharp edge. 1301 This angle is used to control the splitting of the mesh. 1302 A split line exists when the surface normals between two edge connected triangles 1303 are >= `splitting_angle`. 1304 inflection_point_ratio : (float) 1305 An inflection point occurs when the ratio of reduction error between two iterations 1306 is greater than or equal to the `inflection_point_ratio` value. 1307 vertex_degree : (int) 1308 If the number of triangles connected to a vertex exceeds it then the vertex will be split. 1309 1310 Note: 1311 Setting `fraction=0.1` leaves 10% of the original number of vertices 1312 1313 See also: 1314 `decimate()` and `decimate_binned()`. 1315 """ 1316 poly = self.dataset 1317 if n: # N = desired number of points 1318 npt = poly.GetNumberOfPoints() 1319 fraction = n / npt 1320 if fraction >= 1: 1321 return self 1322 1323 decimate = vtki.new("DecimatePro") 1324 decimate.SetPreserveTopology(preserve_topology) 1325 decimate.SetBoundaryVertexDeletion(preserve_boundaries) 1326 if feature_angle: 1327 decimate.SetFeatureAngle(feature_angle) 1328 decimate.SetSplitting(splitting) 1329 decimate.SetSplitAngle(splitting_angle) 1330 decimate.SetInflectionPointRatio(inflection_point_ratio) 1331 if vertex_degree: 1332 decimate.SetDegree(vertex_degree) 1333 1334 decimate.SetTargetReduction(1 - fraction) 1335 decimate.SetInputData(poly) 1336 decimate.Update() 1337 self._update(decimate.GetOutput()) 1338 1339 self.pipeline = OperationNode( 1340 "decimate_pro", 1341 parents=[self], 1342 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1343 ) 1344 return self
Downsample the number of vertices in a mesh to fraction
.
This filter preserves the pointdata
of the input dataset.
Arguments:
- fraction : (float)
The desired target of reduction.
Setting
fraction=0.1
leaves 10% of the original number of vertices. - n : (int)
the desired number of final points (
fraction
is recalculated based on it). - preserve_topology : (bool) If on, mesh splitting and hole elimination will not occur. This may limit the maximum reduction that may be achieved.
- preserve_boundaries : (bool) Turn on/off the deletion of vertices on the boundary of a mesh. Control whether mesh boundaries are preserved during decimation.
- feature_angle : (float) Specify the angle that defines a feature. This angle is used to define what an edge is (i.e., if the surface normal between two adjacent triangles is >= FeatureAngle, an edge exists).
- splitting : (bool) Turn on/off the splitting of the mesh at corners, along edges, at non-manifold points, or anywhere else a split is required. Turning splitting off will better preserve the original topology of the mesh, but you may not obtain the requested reduction.
- splitting_angle : (float)
Specify the angle that defines a sharp edge.
This angle is used to control the splitting of the mesh.
A split line exists when the surface normals between two edge connected triangles
are >=
splitting_angle
. - inflection_point_ratio : (float)
An inflection point occurs when the ratio of reduction error between two iterations
is greater than or equal to the
inflection_point_ratio
value. - vertex_degree : (int) If the number of triangles connected to a vertex exceeds it then the vertex will be split.
Note:
Setting
fraction=0.1
leaves 10% of the original number of vertices
See also:
1346 def decimate_binned(self, divisions=(), use_clustering=False) -> Self: 1347 """ 1348 Downsample the number of vertices in a mesh. 1349 1350 This filter preserves the `celldata` of the input dataset, 1351 if `use_clustering=True` also the `pointdata` will be preserved in the result. 1352 1353 Arguments: 1354 divisions : (list) 1355 number of divisions along x, y and z axes. 1356 auto_adjust : (bool) 1357 if True, the number of divisions is automatically adjusted to 1358 create more uniform cells. 1359 use_clustering : (bool) 1360 use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html) 1361 instead of 1362 [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html). 1363 1364 See also: `decimate()` and `decimate_pro()`. 1365 """ 1366 if use_clustering: 1367 decimate = vtki.new("QuadricClustering") 1368 decimate.CopyCellDataOn() 1369 else: 1370 decimate = vtki.new("BinnedDecimation") 1371 decimate.ProducePointDataOn() 1372 decimate.ProduceCellDataOn() 1373 1374 decimate.SetInputData(self.dataset) 1375 1376 if len(divisions) == 0: 1377 decimate.SetAutoAdjustNumberOfDivisions(1) 1378 else: 1379 decimate.SetAutoAdjustNumberOfDivisions(0) 1380 decimate.SetNumberOfDivisions(divisions) 1381 decimate.Update() 1382 1383 self._update(decimate.GetOutput()) 1384 self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions() 1385 self.pipeline = OperationNode( 1386 "decimate_binned", 1387 parents=[self], 1388 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1389 ) 1390 return self
Downsample the number of vertices in a mesh.
This filter preserves the celldata
of the input dataset,
if use_clustering=True
also the pointdata
will be preserved in the result.
Arguments:
- divisions : (list) number of divisions along x, y and z axes.
- auto_adjust : (bool) if True, the number of divisions is automatically adjusted to create more uniform cells.
- use_clustering : (bool) use vtkQuadricClustering instead of vtkBinnedDecimation.
See also: decimate()
and decimate_pro()
.
1392 def generate_random_points(self, n: int, min_radius=0.0) -> "Points": 1393 """ 1394 Generate `n` uniformly distributed random points 1395 inside the polygonal mesh. 1396 1397 A new point data array is added to the output points 1398 called "OriginalCellID" which contains the index of 1399 the cell ID in which the point was generated. 1400 1401 Arguments: 1402 n : (int) 1403 number of points to generate. 1404 min_radius: (float) 1405 impose a minimum distance between points. 1406 If `min_radius` is set to 0, the points are 1407 generated uniformly at random inside the mesh. 1408 If `min_radius` is set to a positive value, 1409 the points are generated uniformly at random 1410 inside the mesh, but points closer than `min_radius` 1411 to any other point are discarded. 1412 1413 Returns a `vedo.Points` object. 1414 1415 Note: 1416 Consider using `points.probe(msh)` or 1417 `points.interpolate_data_from(msh)` 1418 to interpolate existing mesh data onto the new points. 1419 1420 Example: 1421 ```python 1422 from vedo import * 1423 msh = Mesh(dataurl + "panther.stl").lw(2) 1424 pts = msh.generate_random_points(20000, min_radius=0.5) 1425 print("Original cell ids:", pts.pointdata["OriginalCellID"]) 1426 show(pts, msh, axes=1).close() 1427 ``` 1428 """ 1429 cmesh = self.clone().clean().triangulate().compute_cell_size() 1430 triangles = cmesh.cells 1431 vertices = cmesh.vertices 1432 cumul = np.cumsum(cmesh.celldata["Area"]) 1433 1434 out_pts = [] 1435 orig_cell = [] 1436 for _ in range(n): 1437 # choose a triangle based on area 1438 random_area = np.random.random() * cumul[-1] 1439 it = np.searchsorted(cumul, random_area) 1440 A, B, C = vertices[triangles[it]] 1441 # calculate the random point in the triangle 1442 r1, r2 = np.random.random(2) 1443 if r1 + r2 > 1: 1444 r1 = 1 - r1 1445 r2 = 1 - r2 1446 out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C) 1447 orig_cell.append(it) 1448 nporig_cell = np.array(orig_cell, dtype=np.uint32) 1449 1450 vpts = Points(out_pts) 1451 vpts.pointdata["OriginalCellID"] = nporig_cell 1452 1453 if min_radius > 0: 1454 vpts.subsample(min_radius, absolute=True) 1455 1456 vpts.point_size(5).color("k1") 1457 vpts.name = "RandomPoints" 1458 vpts.pipeline = OperationNode( 1459 "generate_random_points", c="#edabab", parents=[self]) 1460 return vpts
Generate n
uniformly distributed random points
inside the polygonal mesh.
A new point data array is added to the output points called "OriginalCellID" which contains the index of the cell ID in which the point was generated.
Arguments:
- n : (int) number of points to generate.
- min_radius: (float)
impose a minimum distance between points.
If
min_radius
is set to 0, the points are generated uniformly at random inside the mesh. Ifmin_radius
is set to a positive value, the points are generated uniformly at random inside the mesh, but points closer thanmin_radius
to any other point are discarded.
Returns a vedo.Points
object.
Note:
Consider using
points.probe(msh)
orpoints.interpolate_data_from(msh)
to interpolate existing mesh data onto the new points.
Example:
from vedo import *
msh = Mesh(dataurl + "panther.stl").lw(2)
pts = msh.generate_random_points(20000, min_radius=0.5)
print("Original cell ids:", pts.pointdata["OriginalCellID"])
show(pts, msh, axes=1).close()
1462 def delete_cells(self, ids: List[int]) -> Self: 1463 """ 1464 Remove cells from the mesh object by their ID. 1465 Points (vertices) are not removed (you may use `clean()` to remove those). 1466 """ 1467 self.dataset.BuildLinks() 1468 for cid in ids: 1469 self.dataset.DeleteCell(cid) 1470 self.dataset.RemoveDeletedCells() 1471 self.dataset.Modified() 1472 self.mapper.Modified() 1473 self.pipeline = OperationNode( 1474 "delete_cells", 1475 parents=[self], 1476 comment=f"#cells {self.dataset.GetNumberOfCells()}", 1477 ) 1478 return self
Remove cells from the mesh object by their ID.
Points (vertices) are not removed (you may use clean()
to remove those).
1480 def delete_cells_by_point_index(self, indices: List[int]) -> Self: 1481 """ 1482 Delete a list of vertices identified by any of their vertex index. 1483 1484 See also `delete_cells()`. 1485 1486 Examples: 1487 - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py) 1488 1489 ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png) 1490 """ 1491 cell_ids = vtki.vtkIdList() 1492 self.dataset.BuildLinks() 1493 n = 0 1494 for i in np.unique(indices): 1495 self.dataset.GetPointCells(i, cell_ids) 1496 for j in range(cell_ids.GetNumberOfIds()): 1497 self.dataset.DeleteCell(cell_ids.GetId(j)) # flag cell 1498 n += 1 1499 1500 self.dataset.RemoveDeletedCells() 1501 self.dataset.Modified() 1502 self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self]) 1503 return self
Delete a list of vertices identified by any of their vertex index.
See also delete_cells()
.
Examples:
1505 def collapse_edges(self, distance: float, iterations=1) -> Self: 1506 """ 1507 Collapse mesh edges so that are all above `distance`. 1508 1509 Example: 1510 ```python 1511 from vedo import * 1512 np.random.seed(2) 1513 grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1) 1514 grid1.celldata['scalar'] = grid1.cell_centers[:,1] 1515 grid2 = grid1.clone().collapse_edges(0.1) 1516 show(grid1, grid2, N=2, axes=1) 1517 ``` 1518 """ 1519 for _ in range(iterations): 1520 medges = self.edges 1521 pts = self.vertices 1522 newpts = np.array(pts) 1523 moved = [] 1524 for e in medges: 1525 if len(e) == 2: 1526 id0, id1 = e 1527 p0, p1 = pts[id0], pts[id1] 1528 if (np.linalg.norm(p1-p0) < distance 1529 and id0 not in moved 1530 and id1 not in moved 1531 ): 1532 p = (p0 + p1) / 2 1533 newpts[id0] = p 1534 newpts[id1] = p 1535 moved += [id0, id1] 1536 self.vertices = newpts 1537 cpd = vtki.new("CleanPolyData") 1538 cpd.ConvertLinesToPointsOff() 1539 cpd.ConvertPolysToLinesOff() 1540 cpd.ConvertStripsToPolysOff() 1541 cpd.SetInputData(self.dataset) 1542 cpd.Update() 1543 self._update(cpd.GetOutput()) 1544 1545 self.pipeline = OperationNode( 1546 "collapse_edges", 1547 parents=[self], 1548 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1549 ) 1550 return self
Collapse mesh edges so that are all above distance
.
Example:
from vedo import * np.random.seed(2) grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1) grid1.celldata['scalar'] = grid1.cell_centers[:,1] grid2 = grid1.clone().collapse_edges(0.1) show(grid1, grid2, N=2, axes=1)
1552 def adjacency_list(self) -> List[set]: 1553 """ 1554 Computes the adjacency list for mesh edge-graph. 1555 1556 Returns: 1557 a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex 1558 """ 1559 inc = [set()] * self.nvertices 1560 for cell in self.cells: 1561 nc = len(cell) 1562 if nc > 1: 1563 for i in range(nc-1): 1564 ci = cell[i] 1565 inc[ci] = inc[ci].union({cell[i-1], cell[i+1]}) 1566 return inc
Computes the adjacency list for mesh edge-graph.
Returns: a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex
1568 def graph_ball(self, index, n: int) -> set: 1569 """ 1570 Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`. 1571 1572 Arguments: 1573 index : (int) 1574 index of the vertex 1575 n : (int) 1576 radius in the graph metric 1577 1578 Returns: 1579 the set of indices of the vertices which are at most `n` edges from vertex `index`. 1580 """ 1581 if n == 0: 1582 return {index} 1583 else: 1584 al = self.adjacency_list() 1585 ball = {index} 1586 i = 0 1587 while i < n and len(ball) < self.nvertices: 1588 for v in ball: 1589 ball = ball.union(al[v]) 1590 i += 1 1591 return ball
Computes the ball of radius n
in the mesh' edge-graph metric centred in vertex index
.
Arguments:
- index : (int) index of the vertex
- n : (int) radius in the graph metric
Returns:
the set of indices of the vertices which are at most
n
edges from vertexindex
.
1593 def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self: 1594 """ 1595 Adjust mesh point positions using the so-called "Windowed Sinc" method. 1596 1597 Arguments: 1598 niter : (int) 1599 number of iterations. 1600 pass_band : (float) 1601 set the pass_band value for the windowed sinc filter. 1602 edge_angle : (float) 1603 edge angle to control smoothing along edges (either interior or boundary). 1604 feature_angle : (float) 1605 specifies the feature angle for sharp edge identification. 1606 boundary : (bool) 1607 specify if boundary should also be smoothed or kept unmodified 1608 1609 Examples: 1610 - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py) 1611 1612 ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png) 1613 """ 1614 cl = vtki.new("CleanPolyData") 1615 cl.SetInputData(self.dataset) 1616 cl.Update() 1617 smf = vtki.new("WindowedSincPolyDataFilter") 1618 smf.SetInputData(cl.GetOutput()) 1619 smf.SetNumberOfIterations(niter) 1620 smf.SetEdgeAngle(edge_angle) 1621 smf.SetFeatureAngle(feature_angle) 1622 smf.SetPassBand(pass_band) 1623 smf.NormalizeCoordinatesOn() 1624 smf.NonManifoldSmoothingOn() 1625 smf.FeatureEdgeSmoothingOn() 1626 smf.SetBoundarySmoothing(boundary) 1627 smf.Update() 1628 1629 self._update(smf.GetOutput()) 1630 1631 self.pipeline = OperationNode( 1632 "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1633 ) 1634 return self
Adjust mesh point positions using the so-called "Windowed Sinc" method.
Arguments:
- niter : (int) number of iterations.
- pass_band : (float) set the pass_band value for the windowed sinc filter.
- edge_angle : (float) edge angle to control smoothing along edges (either interior or boundary).
- feature_angle : (float) specifies the feature angle for sharp edge identification.
- boundary : (bool) specify if boundary should also be smoothed or kept unmodified
Examples:
1636 def fill_holes(self, size=None) -> Self: 1637 """ 1638 Identifies and fills holes in the input mesh. 1639 Holes are identified by locating boundary edges, linking them together 1640 into loops, and then triangulating the resulting loops. 1641 1642 Arguments: 1643 size : (float) 1644 Approximate limit to the size of the hole that can be filled. 1645 1646 Examples: 1647 - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py) 1648 """ 1649 fh = vtki.new("FillHolesFilter") 1650 if not size: 1651 mb = self.diagonal_size() 1652 size = mb / 10 1653 fh.SetHoleSize(size) 1654 fh.SetInputData(self.dataset) 1655 fh.Update() 1656 1657 self._update(fh.GetOutput()) 1658 1659 self.pipeline = OperationNode( 1660 "fill_holes", 1661 parents=[self], 1662 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1663 ) 1664 return self
Identifies and fills holes in the input mesh. Holes are identified by locating boundary edges, linking them together into loops, and then triangulating the resulting loops.
Arguments:
- size : (float) Approximate limit to the size of the hole that can be filled.
Examples:
1666 def contains(self, point: tuple, tol=1e-05) -> bool: 1667 """ 1668 Return True if point is inside a polydata closed surface. 1669 1670 Note: 1671 if you have many points to check use `inside_points()` instead. 1672 1673 Example: 1674 ```python 1675 from vedo import * 1676 s = Sphere().c('green5').alpha(0.5) 1677 pt = [0.1, 0.2, 0.3] 1678 print("Sphere contains", pt, s.contains(pt)) 1679 show(s, Point(pt), axes=1).close() 1680 ``` 1681 """ 1682 points = vtki.vtkPoints() 1683 points.InsertNextPoint(point) 1684 poly = vtki.vtkPolyData() 1685 poly.SetPoints(points) 1686 sep = vtki.new("SelectEnclosedPoints") 1687 sep.SetTolerance(tol) 1688 sep.CheckSurfaceOff() 1689 sep.SetInputData(poly) 1690 sep.SetSurfaceData(self.dataset) 1691 sep.Update() 1692 return bool(sep.IsInside(0))
Return True if point is inside a polydata closed surface.
Note:
if you have many points to check use
inside_points()
instead.
Example:
from vedo import * s = Sphere().c('green5').alpha(0.5) pt = [0.1, 0.2, 0.3] print("Sphere contains", pt, s.contains(pt)) show(s, Point(pt), axes=1).close()
1694 def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]: 1695 """ 1696 Return the point cloud that is inside mesh surface as a new Points object. 1697 1698 If return_ids is True a list of IDs is returned and in addition input points 1699 are marked by a pointdata array named "IsInside". 1700 1701 Example: 1702 `print(pts.pointdata["IsInside"])` 1703 1704 Examples: 1705 - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 1706 1707 ![](https://vedo.embl.es/images/basic/pca.png) 1708 """ 1709 if isinstance(pts, Points): 1710 poly = pts.dataset 1711 ptsa = pts.vertices 1712 else: 1713 ptsa = np.asarray(pts) 1714 vpoints = vtki.vtkPoints() 1715 vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32)) 1716 poly = vtki.vtkPolyData() 1717 poly.SetPoints(vpoints) 1718 1719 sep = vtki.new("SelectEnclosedPoints") 1720 # sep = vtki.new("ExtractEnclosedPoints() 1721 sep.SetTolerance(tol) 1722 sep.SetInputData(poly) 1723 sep.SetSurfaceData(self.dataset) 1724 sep.SetInsideOut(invert) 1725 sep.Update() 1726 1727 varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints") 1728 mask = vtk2numpy(varr).astype(bool) 1729 ids = np.array(range(len(ptsa)), dtype=int)[mask] 1730 1731 if isinstance(pts, Points): 1732 varr.SetName("IsInside") 1733 pts.dataset.GetPointData().AddArray(varr) 1734 1735 if return_ids: 1736 return ids 1737 1738 pcl = Points(ptsa[ids]) 1739 pcl.name = "InsidePoints" 1740 1741 pcl.pipeline = OperationNode( 1742 "inside_points", 1743 parents=[self, ptsa], 1744 comment=f"#pts {pcl.dataset.GetNumberOfPoints()}", 1745 ) 1746 return pcl
Return the point cloud that is inside mesh surface as a new Points object.
If return_ids is True a list of IDs is returned and in addition input points are marked by a pointdata array named "IsInside".
Example:
print(pts.pointdata["IsInside"])
Examples:
1748 def boundaries( 1749 self, 1750 boundary_edges=True, 1751 manifold_edges=False, 1752 non_manifold_edges=False, 1753 feature_angle=None, 1754 return_point_ids=False, 1755 return_cell_ids=False, 1756 cell_edge=False, 1757 ) -> Union[Self, np.ndarray]: 1758 """ 1759 Return the boundary lines of an input mesh. 1760 Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method. 1761 1762 Arguments: 1763 boundary_edges : (bool) 1764 Turn on/off the extraction of boundary edges. 1765 manifold_edges : (bool) 1766 Turn on/off the extraction of manifold edges. 1767 non_manifold_edges : (bool) 1768 Turn on/off the extraction of non-manifold edges. 1769 feature_angle : (bool) 1770 Specify the min angle btw 2 faces for extracting edges. 1771 return_point_ids : (bool) 1772 return a numpy array of point indices 1773 return_cell_ids : (bool) 1774 return a numpy array of cell indices 1775 cell_edge : (bool) 1776 set to `True` if a cell need to share an edge with 1777 the boundary line, or `False` if a single vertex is enough 1778 1779 Examples: 1780 - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py) 1781 1782 ![](https://vedo.embl.es/images/basic/boundaries.png) 1783 """ 1784 fe = vtki.new("FeatureEdges") 1785 fe.SetBoundaryEdges(boundary_edges) 1786 fe.SetNonManifoldEdges(non_manifold_edges) 1787 fe.SetManifoldEdges(manifold_edges) 1788 try: 1789 fe.SetPassLines(True) # vtk9.2 1790 except AttributeError: 1791 pass 1792 fe.ColoringOff() 1793 fe.SetFeatureEdges(False) 1794 if feature_angle is not None: 1795 fe.SetFeatureEdges(True) 1796 fe.SetFeatureAngle(feature_angle) 1797 1798 if return_point_ids or return_cell_ids: 1799 idf = vtki.new("IdFilter") 1800 idf.SetInputData(self.dataset) 1801 idf.SetPointIdsArrayName("BoundaryIds") 1802 idf.SetPointIds(True) 1803 idf.Update() 1804 1805 fe.SetInputData(idf.GetOutput()) 1806 fe.Update() 1807 1808 vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds") 1809 npid = vtk2numpy(vid).astype(int) 1810 1811 if return_point_ids: 1812 return npid 1813 1814 if return_cell_ids: 1815 n = 1 if cell_edge else 0 1816 inface = [] 1817 for i, face in enumerate(self.cells): 1818 # isin = np.any([vtx in npid for vtx in face]) 1819 isin = 0 1820 for vtx in face: 1821 isin += int(vtx in npid) 1822 if isin > n: 1823 break 1824 if isin > n: 1825 inface.append(i) 1826 return np.array(inface).astype(int) 1827 1828 return self 1829 1830 else: 1831 1832 fe.SetInputData(self.dataset) 1833 fe.Update() 1834 msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off") 1835 msh.name = "MeshBoundaries" 1836 1837 msh.pipeline = OperationNode( 1838 "boundaries", 1839 parents=[self], 1840 shape="octagon", 1841 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 1842 ) 1843 return msh
Return the boundary lines of an input mesh.
Check also vedo.core.CommonAlgorithms.mark_boundaries()
method.
Arguments:
- boundary_edges : (bool) Turn on/off the extraction of boundary edges.
- manifold_edges : (bool) Turn on/off the extraction of manifold edges.
- non_manifold_edges : (bool) Turn on/off the extraction of non-manifold edges.
- feature_angle : (bool) Specify the min angle btw 2 faces for extracting edges.
- return_point_ids : (bool) return a numpy array of point indices
- return_cell_ids : (bool) return a numpy array of cell indices
- cell_edge : (bool)
set to
True
if a cell need to share an edge with the boundary line, orFalse
if a single vertex is enough
Examples:
1845 def imprint(self, loopline, tol=0.01) -> Self: 1846 """ 1847 Imprint the contact surface of one object onto another surface. 1848 1849 Arguments: 1850 loopline : (vedo.Line) 1851 a Line object to be imprinted onto the mesh. 1852 tol : (float) 1853 projection tolerance which controls how close the imprint 1854 surface must be to the target. 1855 1856 Example: 1857 ```python 1858 from vedo import * 1859 grid = Grid()#.triangulate() 1860 circle = Circle(r=0.3, res=24).pos(0.11,0.12) 1861 line = Line(circle, closed=True, lw=4, c='r4') 1862 grid.imprint(line) 1863 show(grid, line, axes=1).close() 1864 ``` 1865 ![](https://vedo.embl.es/images/feats/imprint.png) 1866 """ 1867 loop = vtki.new("ContourLoopExtraction") 1868 loop.SetInputData(loopline.dataset) 1869 loop.Update() 1870 1871 clean_loop = vtki.new("CleanPolyData") 1872 clean_loop.SetInputData(loop.GetOutput()) 1873 clean_loop.Update() 1874 1875 imp = vtki.new("ImprintFilter") 1876 imp.SetTargetData(self.dataset) 1877 imp.SetImprintData(clean_loop.GetOutput()) 1878 imp.SetTolerance(tol) 1879 imp.BoundaryEdgeInsertionOn() 1880 imp.TriangulateOutputOn() 1881 imp.Update() 1882 1883 self._update(imp.GetOutput()) 1884 1885 self.pipeline = OperationNode( 1886 "imprint", 1887 parents=[self], 1888 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1889 ) 1890 return self
Imprint the contact surface of one object onto another surface.
Arguments:
- loopline : (vedo.Line) a Line object to be imprinted onto the mesh.
- tol : (float) projection tolerance which controls how close the imprint surface must be to the target.
Example:
from vedo import * grid = Grid()#.triangulate() circle = Circle(r=0.3, res=24).pos(0.11,0.12) line = Line(circle, closed=True, lw=4, c='r4') grid.imprint(line) show(grid, line, axes=1).close()
1892 def connected_vertices(self, index: int) -> List[int]: 1893 """Find all vertices connected to an input vertex specified by its index. 1894 1895 Examples: 1896 - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py) 1897 1898 ![](https://vedo.embl.es/images/basic/connVtx.png) 1899 """ 1900 poly = self.dataset 1901 1902 cell_idlist = vtki.vtkIdList() 1903 poly.GetPointCells(index, cell_idlist) 1904 1905 idxs = [] 1906 for i in range(cell_idlist.GetNumberOfIds()): 1907 point_idlist = vtki.vtkIdList() 1908 poly.GetCellPoints(cell_idlist.GetId(i), point_idlist) 1909 for j in range(point_idlist.GetNumberOfIds()): 1910 idj = point_idlist.GetId(j) 1911 if idj == index: 1912 continue 1913 if idj in idxs: 1914 continue 1915 idxs.append(idj) 1916 1917 return idxs
1919 def extract_cells(self, ids: List[int]) -> Self: 1920 """ 1921 Extract a subset of cells from a mesh and return it as a new mesh. 1922 """ 1923 selectCells = vtki.new("SelectionNode") 1924 selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL) 1925 selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES) 1926 idarr = vtki.vtkIdTypeArray() 1927 idarr.SetNumberOfComponents(1) 1928 idarr.SetNumberOfValues(len(ids)) 1929 for i, v in enumerate(ids): 1930 idarr.SetValue(i, v) 1931 selectCells.SetSelectionList(idarr) 1932 1933 selection = vtki.new("Selection") 1934 selection.AddNode(selectCells) 1935 1936 extractSelection = vtki.new("ExtractSelection") 1937 extractSelection.SetInputData(0, self.dataset) 1938 extractSelection.SetInputData(1, selection) 1939 extractSelection.Update() 1940 1941 gf = vtki.new("GeometryFilter") 1942 gf.SetInputData(extractSelection.GetOutput()) 1943 gf.Update() 1944 msh = Mesh(gf.GetOutput()) 1945 msh.copy_properties_from(self) 1946 return msh
Extract a subset of cells from a mesh and return it as a new mesh.
1948 def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]: 1949 """Find all cellls connected to an input vertex specified by its index.""" 1950 1951 # Find all cells connected to point index 1952 dpoly = self.dataset 1953 idlist = vtki.vtkIdList() 1954 dpoly.GetPointCells(index, idlist) 1955 1956 ids = vtki.vtkIdTypeArray() 1957 ids.SetNumberOfComponents(1) 1958 rids = [] 1959 for k in range(idlist.GetNumberOfIds()): 1960 cid = idlist.GetId(k) 1961 ids.InsertNextValue(cid) 1962 rids.append(int(cid)) 1963 if return_ids: 1964 return rids 1965 1966 selection_node = vtki.new("SelectionNode") 1967 selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL) 1968 selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES) 1969 selection_node.SetSelectionList(ids) 1970 selection = vtki.new("Selection") 1971 selection.AddNode(selection_node) 1972 extractSelection = vtki.new("ExtractSelection") 1973 extractSelection.SetInputData(0, dpoly) 1974 extractSelection.SetInputData(1, selection) 1975 extractSelection.Update() 1976 gf = vtki.new("GeometryFilter") 1977 gf.SetInputData(extractSelection.GetOutput()) 1978 gf.Update() 1979 return Mesh(gf.GetOutput()).lw(1)
Find all cellls connected to an input vertex specified by its index.
1981 def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self: 1982 """ 1983 Return a new line `Mesh` which corresponds to the outer `silhouette` 1984 of the input as seen along a specified `direction`, this can also be 1985 a `vtkCamera` object. 1986 1987 Arguments: 1988 direction : (list) 1989 viewpoint direction vector. 1990 If `None` this is guessed by looking at the minimum 1991 of the sides of the bounding box. 1992 border_edges : (bool) 1993 enable or disable generation of border edges 1994 feature_angle : (float) 1995 minimal angle for sharp edges detection. 1996 If set to `False` the functionality is disabled. 1997 1998 Examples: 1999 - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py) 2000 2001 ![](https://vedo.embl.es/images/basic/silhouette1.png) 2002 """ 2003 sil = vtki.new("PolyDataSilhouette") 2004 sil.SetInputData(self.dataset) 2005 sil.SetBorderEdges(border_edges) 2006 if feature_angle is False: 2007 sil.SetEnableFeatureAngle(0) 2008 else: 2009 sil.SetEnableFeatureAngle(1) 2010 sil.SetFeatureAngle(feature_angle) 2011 2012 if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera: 2013 sil.SetCamera(vedo.plotter_instance.camera) 2014 m = Mesh() 2015 m.mapper.SetInputConnection(sil.GetOutputPort()) 2016 2017 elif isinstance(direction, vtki.vtkCamera): 2018 sil.SetCamera(direction) 2019 m = Mesh() 2020 m.mapper.SetInputConnection(sil.GetOutputPort()) 2021 2022 elif direction == "2d": 2023 sil.SetVector(3.4, 4.5, 5.6) # random 2024 sil.SetDirectionToSpecifiedVector() 2025 sil.Update() 2026 m = Mesh(sil.GetOutput()) 2027 2028 elif is_sequence(direction): 2029 sil.SetVector(direction) 2030 sil.SetDirectionToSpecifiedVector() 2031 sil.Update() 2032 m = Mesh(sil.GetOutput()) 2033 else: 2034 vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}") 2035 vedo.logger.error("first render the scene with show() or specify camera/direction") 2036 return self 2037 2038 m.lw(2).c((0, 0, 0)).lighting("off") 2039 m.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2040 m.pipeline = OperationNode("silhouette", parents=[self]) 2041 m.name = "Silhouette" 2042 return m
Return a new line Mesh
which corresponds to the outer silhouette
of the input as seen along a specified direction
, this can also be
a vtkCamera
object.
Arguments:
- direction : (list)
viewpoint direction vector.
If
None
this is guessed by looking at the minimum of the sides of the bounding box. - border_edges : (bool) enable or disable generation of border edges
- feature_angle : (float)
minimal angle for sharp edges detection.
If set to
False
the functionality is disabled.
Examples:
2044 def isobands(self, n=10, vmin=None, vmax=None) -> Self: 2045 """ 2046 Return a new `Mesh` representing the isobands of the active scalars. 2047 This is a new mesh where the scalar is now associated to cell faces and 2048 used to colorize the mesh. 2049 2050 Arguments: 2051 n : (int) 2052 number of isobands in the range 2053 vmin : (float) 2054 minimum of the range 2055 vmax : (float) 2056 maximum of the range 2057 2058 Examples: 2059 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2060 """ 2061 r0, r1 = self.dataset.GetScalarRange() 2062 if vmin is None: 2063 vmin = r0 2064 if vmax is None: 2065 vmax = r1 2066 2067 # -------------------------------- 2068 bands = [] 2069 dx = (vmax - vmin) / float(n) 2070 b = [vmin, vmin + dx / 2.0, vmin + dx] 2071 i = 0 2072 while i < n: 2073 bands.append(b) 2074 b = [b[0] + dx, b[1] + dx, b[2] + dx] 2075 i += 1 2076 2077 # annotate, use the midpoint of the band as the label 2078 lut = self.mapper.GetLookupTable() 2079 labels = [] 2080 for b in bands: 2081 labels.append("{:4.2f}".format(b[1])) 2082 values = vtki.vtkVariantArray() 2083 for la in labels: 2084 values.InsertNextValue(vtki.vtkVariant(la)) 2085 for i in range(values.GetNumberOfTuples()): 2086 lut.SetAnnotation(i, values.GetValue(i).ToString()) 2087 2088 bcf = vtki.new("BandedPolyDataContourFilter") 2089 bcf.SetInputData(self.dataset) 2090 # Use either the minimum or maximum value for each band. 2091 for i, band in enumerate(bands): 2092 bcf.SetValue(i, band[2]) 2093 # We will use an indexed lookup table. 2094 bcf.SetScalarModeToIndex() 2095 bcf.GenerateContourEdgesOff() 2096 bcf.Update() 2097 bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands") 2098 2099 m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True) 2100 m1.mapper.SetLookupTable(lut) 2101 m1.mapper.SetScalarRange(lut.GetRange()) 2102 m1.pipeline = OperationNode("isobands", parents=[self]) 2103 m1.name = "IsoBands" 2104 return m1
Return a new Mesh
representing the isobands of the active scalars.
This is a new mesh where the scalar is now associated to cell faces and
used to colorize the mesh.
Arguments:
- n : (int) number of isobands in the range
- vmin : (float) minimum of the range
- vmax : (float) maximum of the range
Examples:
2106 def isolines(self, n=10, vmin=None, vmax=None) -> Self: 2107 """ 2108 Return a new `Mesh` representing the isolines of the active scalars. 2109 2110 Arguments: 2111 n : (int) 2112 number of isolines in the range 2113 vmin : (float) 2114 minimum of the range 2115 vmax : (float) 2116 maximum of the range 2117 2118 Examples: 2119 - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py) 2120 2121 ![](https://vedo.embl.es/images/pyplot/isolines.png) 2122 """ 2123 bcf = vtki.new("ContourFilter") 2124 bcf.SetInputData(self.dataset) 2125 r0, r1 = self.dataset.GetScalarRange() 2126 if vmin is None: 2127 vmin = r0 2128 if vmax is None: 2129 vmax = r1 2130 bcf.GenerateValues(n, vmin, vmax) 2131 bcf.Update() 2132 sf = vtki.new("Stripper") 2133 sf.SetJoinContiguousSegments(True) 2134 sf.SetInputData(bcf.GetOutput()) 2135 sf.Update() 2136 cl = vtki.new("CleanPolyData") 2137 cl.SetInputData(sf.GetOutput()) 2138 cl.Update() 2139 msh = Mesh(cl.GetOutput(), c="k").lighting("off") 2140 msh.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2141 msh.pipeline = OperationNode("isolines", parents=[self]) 2142 msh.name = "IsoLines" 2143 return msh
Return a new Mesh
representing the isolines of the active scalars.
Arguments:
- n : (int) number of isolines in the range
- vmin : (float) minimum of the range
- vmax : (float) maximum of the range
Examples:
2145 def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self: 2146 """ 2147 Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices. 2148 The input dataset is swept around the z-axis to create new polygonal primitives. 2149 For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus. 2150 2151 You can control whether the sweep of a 2D object (i.e., polygon or triangle strip) 2152 is capped with the generating geometry. 2153 Also, you can control the angle of rotation, and whether translation along the z-axis 2154 is performed along with the rotation. (Translation is useful for creating "springs"). 2155 You also can adjust the radius of the generating geometry using the "dR" keyword. 2156 2157 The skirt is generated by locating certain topological features. 2158 Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips) 2159 generate surfaces. This is true also of lines or polylines. Vertices generate lines. 2160 2161 This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses; 2162 or translational/rotational symmetric objects like springs or corkscrews. 2163 2164 Arguments: 2165 zshift : (float) 2166 shift along z axis. 2167 direction : (list) 2168 extrusion direction in the xy plane. 2169 note that zshift is forced to be the 3rd component of direction, 2170 which is therefore ignored. 2171 rotation : (float) 2172 set the angle of rotation. 2173 dr : (float) 2174 set the radius variation in absolute units. 2175 cap : (bool) 2176 enable or disable capping. 2177 res : (int) 2178 set the resolution of the generating geometry. 2179 2180 Warning: 2181 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result 2182 in two separate surfaces if capping is on, or no surface if capping is off. 2183 2184 Examples: 2185 - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py) 2186 2187 ![](https://vedo.embl.es/images/basic/extrude.png) 2188 """ 2189 rf = vtki.new("RotationalExtrusionFilter") 2190 # rf = vtki.new("LinearExtrusionFilter") 2191 rf.SetInputData(self.dataset) # must not be transformed 2192 rf.SetResolution(res) 2193 rf.SetCapping(cap) 2194 rf.SetAngle(rotation) 2195 rf.SetTranslation(zshift) 2196 rf.SetDeltaRadius(dr) 2197 rf.Update() 2198 2199 # convert triangle strips to polygonal data 2200 tris = vtki.new("TriangleFilter") 2201 tris.SetInputData(rf.GetOutput()) 2202 tris.Update() 2203 2204 m = Mesh(tris.GetOutput()) 2205 2206 if len(direction) > 1: 2207 p = self.pos() 2208 LT = vedo.LinearTransform() 2209 LT.translate(-p) 2210 LT.concatenate([ 2211 [1, 0, direction[0]], 2212 [0, 1, direction[1]], 2213 [0, 0, 1] 2214 ]) 2215 LT.translate(p) 2216 m.apply_transform(LT) 2217 2218 m.copy_properties_from(self).flat().lighting("default") 2219 m.pipeline = OperationNode( 2220 "extrude", parents=[self], 2221 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2222 ) 2223 m.name = "ExtrudedMesh" 2224 return m
Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices. The input dataset is swept around the z-axis to create new polygonal primitives. For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.
You can control whether the sweep of a 2D object (i.e., polygon or triangle strip) is capped with the generating geometry. Also, you can control the angle of rotation, and whether translation along the z-axis is performed along with the rotation. (Translation is useful for creating "springs"). You also can adjust the radius of the generating geometry using the "dR" keyword.
The skirt is generated by locating certain topological features. Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips) generate surfaces. This is true also of lines or polylines. Vertices generate lines.
This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses; or translational/rotational symmetric objects like springs or corkscrews.
Arguments:
- zshift : (float) shift along z axis.
- direction : (list) extrusion direction in the xy plane. note that zshift is forced to be the 3rd component of direction, which is therefore ignored.
- rotation : (float) set the angle of rotation.
- dr : (float) set the radius variation in absolute units.
- cap : (bool) enable or disable capping.
- res : (int) set the resolution of the generating geometry.
Warning:
Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate surfaces if capping is on, or no surface if capping is off.
Examples:
2226 def extrude_and_trim_with( 2227 self, 2228 surface: "Mesh", 2229 direction=(), 2230 strategy="all", 2231 cap=True, 2232 cap_strategy="max", 2233 ) -> Self: 2234 """ 2235 Extrude a Mesh and trim it with an input surface mesh. 2236 2237 Arguments: 2238 surface : (Mesh) 2239 the surface mesh to trim with. 2240 direction : (list) 2241 extrusion direction in the xy plane. 2242 strategy : (str) 2243 either "boundary_edges" or "all_edges". 2244 cap : (bool) 2245 enable or disable capping. 2246 cap_strategy : (str) 2247 either "intersection", "minimum_distance", "maximum_distance", "average_distance". 2248 2249 The input Mesh is swept along a specified direction forming a "skirt" 2250 from the boundary edges 2D primitives (i.e., edges used by only one polygon); 2251 and/or from vertices and lines. 2252 The extent of the sweeping is limited by a second input: defined where 2253 the sweep intersects a user-specified surface. 2254 2255 Capping of the extrusion can be enabled. 2256 In this case the input, generating primitive is copied inplace as well 2257 as to the end of the extrusion skirt. 2258 (See warnings below on what happens if the intersecting sweep does not 2259 intersect, or partially intersects the trim surface.) 2260 2261 Note that this method operates in two fundamentally different modes 2262 based on the extrusion strategy. 2263 If the strategy is "boundary_edges", then only the boundary edges of the input's 2264 2D primitives are extruded (verts and lines are extruded to generate lines and quads). 2265 However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives 2266 is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads). 2267 2268 Warning: 2269 The extrusion direction is assumed to define an infinite line. 2270 The intersection with the trim surface is along a ray from the - to + direction, 2271 however only the first intersection is taken. 2272 Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate 2273 surfaces if capping is on and "boundary_edges" enabled, 2274 or no surface if capping is off and "boundary_edges" is enabled. 2275 If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, 2276 then no output for that primitive will be generated. In extreme cases, it is possible that no output 2277 whatsoever will be generated. 2278 2279 Example: 2280 ```python 2281 from vedo import * 2282 sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5') 2283 circle = Circle([0,0,0], r=2, res=100).color('b6') 2284 extruded_circle = circle.extrude_and_trim_with( 2285 sphere, 2286 direction=[0,-0.2,1], 2287 strategy="bound", 2288 cap=True, 2289 cap_strategy="intersection", 2290 ) 2291 circle.lw(3).color("tomato").shift(dz=-0.1) 2292 show(circle, sphere, extruded_circle, axes=1).close() 2293 ``` 2294 """ 2295 trimmer = vtki.new("TrimmedExtrusionFilter") 2296 trimmer.SetInputData(self.dataset) 2297 trimmer.SetCapping(cap) 2298 trimmer.SetExtrusionDirection(direction) 2299 trimmer.SetTrimSurfaceData(surface.dataset) 2300 if "bound" in strategy: 2301 trimmer.SetExtrusionStrategyToBoundaryEdges() 2302 elif "all" in strategy: 2303 trimmer.SetExtrusionStrategyToAllEdges() 2304 else: 2305 vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}") 2306 # print (trimmer.GetExtrusionStrategy()) 2307 2308 if "intersect" in cap_strategy: 2309 trimmer.SetCappingStrategyToIntersection() 2310 elif "min" in cap_strategy: 2311 trimmer.SetCappingStrategyToMinimumDistance() 2312 elif "max" in cap_strategy: 2313 trimmer.SetCappingStrategyToMaximumDistance() 2314 elif "ave" in cap_strategy: 2315 trimmer.SetCappingStrategyToAverageDistance() 2316 else: 2317 vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}") 2318 # print (trimmer.GetCappingStrategy()) 2319 2320 trimmer.Update() 2321 2322 m = Mesh(trimmer.GetOutput()) 2323 m.copy_properties_from(self).flat().lighting("default") 2324 m.pipeline = OperationNode( 2325 "extrude_and_trim", parents=[self, surface], 2326 comment=f"#pts {m.dataset.GetNumberOfPoints()}" 2327 ) 2328 m.name = "ExtrudedAndTrimmedMesh" 2329 return m
Extrude a Mesh and trim it with an input surface mesh.
Arguments:
- surface : (Mesh) the surface mesh to trim with.
- direction : (list) extrusion direction in the xy plane.
- strategy : (str) either "boundary_edges" or "all_edges".
- cap : (bool) enable or disable capping.
- cap_strategy : (str) either "intersection", "minimum_distance", "maximum_distance", "average_distance".
The input Mesh is swept along a specified direction forming a "skirt" from the boundary edges 2D primitives (i.e., edges used by only one polygon); and/or from vertices and lines. The extent of the sweeping is limited by a second input: defined where the sweep intersects a user-specified surface.
Capping of the extrusion can be enabled. In this case the input, generating primitive is copied inplace as well as to the end of the extrusion skirt. (See warnings below on what happens if the intersecting sweep does not intersect, or partially intersects the trim surface.)
Note that this method operates in two fundamentally different modes based on the extrusion strategy. If the strategy is "boundary_edges", then only the boundary edges of the input's 2D primitives are extruded (verts and lines are extruded to generate lines and quads). However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).
Warning:
The extrusion direction is assumed to define an infinite line. The intersection with the trim surface is along a ray from the - to + direction, however only the first intersection is taken. Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate surfaces if capping is on and "boundary_edges" enabled, or no surface if capping is off and "boundary_edges" is enabled. If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, then no output for that primitive will be generated. In extreme cases, it is possible that no output whatsoever will be generated.
Example:
from vedo import * sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5') circle = Circle([0,0,0], r=2, res=100).color('b6') extruded_circle = circle.extrude_and_trim_with( sphere, direction=[0,-0.2,1], strategy="bound", cap=True, cap_strategy="intersection", ) circle.lw(3).color("tomato").shift(dz=-0.1) show(circle, sphere, extruded_circle, axes=1).close()
2331 def split( 2332 self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True 2333 ) -> List[Self]: 2334 """ 2335 Split a mesh by connectivity and order the pieces by increasing area. 2336 2337 Arguments: 2338 maxdepth : (int) 2339 only consider this maximum number of mesh parts. 2340 flag : (bool) 2341 if set to True return the same single object, 2342 but add a "RegionId" array to flag the mesh subparts 2343 must_share_edge : (bool) 2344 if True, mesh regions that only share single points will be split. 2345 sort_by_area : (bool) 2346 if True, sort the mesh parts by decreasing area. 2347 2348 Examples: 2349 - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py) 2350 2351 ![](https://vedo.embl.es/images/advanced/splitmesh.png) 2352 """ 2353 pd = self.dataset 2354 if must_share_edge: 2355 if pd.GetNumberOfPolys() == 0: 2356 vedo.logger.warning("in split(): no polygons found. Skip.") 2357 return [self] 2358 cf = vtki.new("PolyDataEdgeConnectivityFilter") 2359 cf.BarrierEdgesOff() 2360 else: 2361 cf = vtki.new("PolyDataConnectivityFilter") 2362 2363 cf.SetInputData(pd) 2364 cf.SetExtractionModeToAllRegions() 2365 cf.SetColorRegions(True) 2366 cf.Update() 2367 out = cf.GetOutput() 2368 2369 if not out.GetNumberOfPoints(): 2370 return [self] 2371 2372 if flag: 2373 self.pipeline = OperationNode("split mesh", parents=[self]) 2374 self._update(out) 2375 return [self] 2376 2377 msh = Mesh(out) 2378 if must_share_edge: 2379 arr = msh.celldata["RegionId"] 2380 on = "cells" 2381 else: 2382 arr = msh.pointdata["RegionId"] 2383 on = "points" 2384 2385 alist = [] 2386 for t in range(max(arr) + 1): 2387 if t == maxdepth: 2388 break 2389 suba = msh.clone().threshold("RegionId", t, t, on=on) 2390 if sort_by_area: 2391 area = suba.area() 2392 else: 2393 area = 0 # dummy 2394 suba.name = "MeshRegion" + str(t) 2395 alist.append([suba, area]) 2396 2397 if sort_by_area: 2398 alist.sort(key=lambda x: x[1]) 2399 alist.reverse() 2400 2401 blist = [] 2402 for i, l in enumerate(alist): 2403 l[0].color(i + 1).phong() 2404 l[0].mapper.ScalarVisibilityOff() 2405 blist.append(l[0]) 2406 if i < 10: 2407 l[0].pipeline = OperationNode( 2408 f"split mesh {i}", 2409 parents=[self], 2410 comment=f"#pts {l[0].dataset.GetNumberOfPoints()}", 2411 ) 2412 return blist
Split a mesh by connectivity and order the pieces by increasing area.
Arguments:
- maxdepth : (int) only consider this maximum number of mesh parts.
- flag : (bool) if set to True return the same single object, but add a "RegionId" array to flag the mesh subparts
- must_share_edge : (bool) if True, mesh regions that only share single points will be split.
- sort_by_area : (bool) if True, sort the mesh parts by decreasing area.
Examples:
2414 def extract_largest_region(self) -> Self: 2415 """ 2416 Extract the largest connected part of a mesh and discard all the smaller pieces. 2417 2418 Examples: 2419 - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py) 2420 """ 2421 conn = vtki.new("PolyDataConnectivityFilter") 2422 conn.SetExtractionModeToLargestRegion() 2423 conn.ScalarConnectivityOff() 2424 conn.SetInputData(self.dataset) 2425 conn.Update() 2426 2427 m = Mesh(conn.GetOutput()) 2428 m.copy_properties_from(self) 2429 m.pipeline = OperationNode( 2430 "extract_largest_region", 2431 parents=[self], 2432 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2433 ) 2434 m.name = "MeshLargestRegion" 2435 return m
Extract the largest connected part of a mesh and discard all the smaller pieces.
Examples:
2437 def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self: 2438 """Volumetric union, intersection and subtraction of surfaces. 2439 2440 Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`. 2441 2442 Two possible algorithms are available. 2443 Setting `method` to 0 (the default) uses the boolean operation algorithm 2444 written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01); 2445 setting `method` to 1 will use the "loop" boolean algorithm 2446 written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015). 2447 2448 Use `tol` to specify the absolute tolerance used to determine 2449 when the distance between two points is considered to be zero (defaults to 1e-6). 2450 2451 Example: 2452 - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py) 2453 2454 ![](https://vedo.embl.es/images/basic/boolean.png) 2455 """ 2456 if method == 0: 2457 bf = vtki.new("BooleanOperationPolyDataFilter") 2458 elif method == 1: 2459 bf = vtki.new("LoopBooleanPolyDataFilter") 2460 else: 2461 raise ValueError(f"Unknown method={method}") 2462 2463 poly1 = self.compute_normals().dataset 2464 poly2 = mesh2.compute_normals().dataset 2465 2466 if operation.lower() in ("plus", "+"): 2467 bf.SetOperationToUnion() 2468 elif operation.lower() == "intersect": 2469 bf.SetOperationToIntersection() 2470 elif operation.lower() in ("minus", "-"): 2471 bf.SetOperationToDifference() 2472 2473 if tol: 2474 bf.SetTolerance(tol) 2475 2476 bf.SetInputData(0, poly1) 2477 bf.SetInputData(1, poly2) 2478 bf.Update() 2479 2480 msh = Mesh(bf.GetOutput(), c=None) 2481 msh.flat() 2482 2483 msh.pipeline = OperationNode( 2484 "boolean " + operation, 2485 parents=[self, mesh2], 2486 shape="cylinder", 2487 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2488 ) 2489 msh.name = self.name + operation + mesh2.name 2490 return msh
Volumetric union, intersection and subtraction of surfaces.
Use operation
for the allowed operations ['plus', 'intersect', 'minus']
.
Two possible algorithms are available.
Setting method
to 0 (the default) uses the boolean operation algorithm
written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01);
setting method
to 1 will use the "loop" boolean algorithm
written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015).
Use tol
to specify the absolute tolerance used to determine
when the distance between two points is considered to be zero (defaults to 1e-6).
Example:
2492 def intersect_with(self, mesh2, tol=1e-06) -> Self: 2493 """ 2494 Intersect this Mesh with the input surface to return a set of lines. 2495 2496 Examples: 2497 - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py) 2498 2499 ![](https://vedo.embl.es/images/basic/surfIntersect.png) 2500 """ 2501 bf = vtki.new("IntersectionPolyDataFilter") 2502 bf.SetGlobalWarningDisplay(0) 2503 bf.SetTolerance(tol) 2504 bf.SetInputData(0, self.dataset) 2505 bf.SetInputData(1, mesh2.dataset) 2506 bf.Update() 2507 msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off") 2508 msh.properties.SetLineWidth(3) 2509 msh.pipeline = OperationNode( 2510 "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}" 2511 ) 2512 msh.name = "SurfaceIntersection" 2513 return msh
2515 def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]: 2516 """ 2517 Return the list of points intersecting the mesh 2518 along the segment defined by two points `p0` and `p1`. 2519 2520 Use `return_ids` to return the cell ids along with point coords 2521 2522 Example: 2523 ```python 2524 from vedo import * 2525 s = Spring() 2526 pts = s.intersect_with_line([0,0,0], [1,0.1,0]) 2527 ln = Line([0,0,0], [1,0.1,0], c='blue') 2528 ps = Points(pts, r=10, c='r') 2529 show(s, ln, ps, bg='white').close() 2530 ``` 2531 ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png) 2532 """ 2533 if isinstance(p0, Points): 2534 p0, p1 = p0.vertices 2535 2536 if not self.line_locator: 2537 self.line_locator = vtki.new("OBBTree") 2538 self.line_locator.SetDataSet(self.dataset) 2539 if not tol: 2540 tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000 2541 self.line_locator.SetTolerance(tol) 2542 self.line_locator.BuildLocator() 2543 2544 vpts = vtki.vtkPoints() 2545 idlist = vtki.vtkIdList() 2546 self.line_locator.IntersectWithLine(p0, p1, vpts, idlist) 2547 pts = [] 2548 for i in range(vpts.GetNumberOfPoints()): 2549 intersection: MutableSequence[float] = [0, 0, 0] 2550 vpts.GetPoint(i, intersection) 2551 pts.append(intersection) 2552 pts2 = np.array(pts) 2553 2554 if return_ids: 2555 pts_ids = [] 2556 for i in range(idlist.GetNumberOfIds()): 2557 cid = idlist.GetId(i) 2558 pts_ids.append(cid) 2559 return (pts2, np.array(pts_ids).astype(np.uint32)) 2560 2561 return pts2
Return the list of points intersecting the mesh
along the segment defined by two points p0
and p1
.
Use return_ids
to return the cell ids along with point coords
Example:
from vedo import * s = Spring() pts = s.intersect_with_line([0,0,0], [1,0.1,0]) ln = Line([0,0,0], [1,0.1,0], c='blue') ps = Points(pts, r=10, c='r') show(s, ln, ps, bg='white').close()
2563 def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self: 2564 """ 2565 Intersect this Mesh with a plane to return a set of lines. 2566 2567 Example: 2568 ```python 2569 from vedo import * 2570 sph = Sphere() 2571 mi = sph.clone().intersect_with_plane().join() 2572 print(mi.lines) 2573 show(sph, mi, axes=1).close() 2574 ``` 2575 ![](https://vedo.embl.es/images/feats/intersect_plane.png) 2576 """ 2577 plane = vtki.new("Plane") 2578 plane.SetOrigin(origin) 2579 plane.SetNormal(normal) 2580 2581 cutter = vtki.new("PolyDataPlaneCutter") 2582 cutter.SetInputData(self.dataset) 2583 cutter.SetPlane(plane) 2584 cutter.InterpolateAttributesOn() 2585 cutter.ComputeNormalsOff() 2586 cutter.Update() 2587 2588 msh = Mesh(cutter.GetOutput()) 2589 msh.c('k').lw(3).lighting("off") 2590 msh.pipeline = OperationNode( 2591 "intersect_with_plan", 2592 parents=[self], 2593 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2594 ) 2595 msh.name = "PlaneIntersection" 2596 return msh
Intersect this Mesh with a plane to return a set of lines.
Example:
from vedo import * sph = Sphere() mi = sph.clone().intersect_with_plane().join() print(mi.lines) show(sph, mi, axes=1).close()
2598 def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]: 2599 """ 2600 Cut/clip a closed surface mesh with a collection of planes. 2601 This will produce a new closed surface by creating new polygonal 2602 faces where the input surface hits the planes. 2603 2604 The orientation of the polygons that form the surface is important. 2605 Polygons have a front face and a back face, and it's the back face that defines 2606 the interior or "solid" region of the closed surface. 2607 When a plane cuts through a "solid" region, a new cut face is generated, 2608 but not when a clipping plane cuts through a hole or "empty" region. 2609 This distinction is crucial when dealing with complex surfaces. 2610 Note that if a simple surface has its back faces pointing outwards, 2611 then that surface defines a hole in a potentially infinite solid. 2612 2613 Non-manifold surfaces should not be used with this method. 2614 2615 Arguments: 2616 origins : (list) 2617 list of plane origins 2618 normals : (list) 2619 list of plane normals 2620 invert : (bool) 2621 invert the clipping. 2622 return_assembly : (bool) 2623 return the cap and the clipped surfaces as a `vedo.Assembly`. 2624 2625 Example: 2626 ```python 2627 from vedo import * 2628 s = Sphere(res=50).linewidth(1) 2629 origins = [[-0.7, 0, 0], [0, -0.6, 0]] 2630 normals = [[-1, 0, 0], [0, -1, 0]] 2631 s.cut_closed_surface(origins, normals) 2632 show(s, axes=1).close() 2633 ``` 2634 """ 2635 planes = vtki.new("PlaneCollection") 2636 for p, s in zip(origins, normals): 2637 plane = vtki.vtkPlane() 2638 plane.SetOrigin(vedo.utils.make3d(p)) 2639 plane.SetNormal(vedo.utils.make3d(s)) 2640 planes.AddItem(plane) 2641 clipper = vtki.new("ClipClosedSurface") 2642 clipper.SetInputData(self.dataset) 2643 clipper.SetClippingPlanes(planes) 2644 clipper.PassPointDataOn() 2645 clipper.GenerateFacesOn() 2646 clipper.SetScalarModeToLabels() 2647 clipper.TriangulationErrorDisplayOn() 2648 clipper.SetInsideOut(not invert) 2649 2650 if return_assembly: 2651 clipper.GenerateClipFaceOutputOn() 2652 clipper.Update() 2653 parts = [] 2654 for i in range(clipper.GetNumberOfOutputPorts()): 2655 msh = Mesh(clipper.GetOutput(i)) 2656 msh.copy_properties_from(self) 2657 msh.name = "CutClosedSurface" 2658 msh.pipeline = OperationNode( 2659 "cut_closed_surface", 2660 parents=[self], 2661 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2662 ) 2663 parts.append(msh) 2664 asse = vedo.Assembly(parts) 2665 asse.name = "CutClosedSurface" 2666 return asse 2667 2668 else: 2669 clipper.GenerateClipFaceOutputOff() 2670 clipper.Update() 2671 self._update(clipper.GetOutput()) 2672 self.flat() 2673 self.name = "CutClosedSurface" 2674 self.pipeline = OperationNode( 2675 "cut_closed_surface", 2676 parents=[self], 2677 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 2678 ) 2679 return self
Cut/clip a closed surface mesh with a collection of planes. This will produce a new closed surface by creating new polygonal faces where the input surface hits the planes.
The orientation of the polygons that form the surface is important. Polygons have a front face and a back face, and it's the back face that defines the interior or "solid" region of the closed surface. When a plane cuts through a "solid" region, a new cut face is generated, but not when a clipping plane cuts through a hole or "empty" region. This distinction is crucial when dealing with complex surfaces. Note that if a simple surface has its back faces pointing outwards, then that surface defines a hole in a potentially infinite solid.
Non-manifold surfaces should not be used with this method.
Arguments:
- origins : (list) list of plane origins
- normals : (list) list of plane normals
- invert : (bool) invert the clipping.
- return_assembly : (bool)
return the cap and the clipped surfaces as a
vedo.Assembly
.
Example:
from vedo import * s = Sphere(res=50).linewidth(1) origins = [[-0.7, 0, 0], [0, -0.6, 0]] normals = [[-1, 0, 0], [0, -1, 0]] s.cut_closed_surface(origins, normals) show(s, axes=1).close()
2681 def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]: 2682 """ 2683 Collide this Mesh with the input surface. 2684 Information is stored in `ContactCells1` and `ContactCells2`. 2685 """ 2686 ipdf = vtki.new("CollisionDetectionFilter") 2687 # ipdf.SetGlobalWarningDisplay(0) 2688 2689 transform0 = vtki.vtkTransform() 2690 transform1 = vtki.vtkTransform() 2691 2692 # ipdf.SetBoxTolerance(tol) 2693 ipdf.SetCellTolerance(tol) 2694 ipdf.SetInputData(0, self.dataset) 2695 ipdf.SetInputData(1, mesh2.dataset) 2696 ipdf.SetTransform(0, transform0) 2697 ipdf.SetTransform(1, transform1) 2698 if return_bool: 2699 ipdf.SetCollisionModeToFirstContact() 2700 else: 2701 ipdf.SetCollisionModeToAllContacts() 2702 ipdf.Update() 2703 2704 if return_bool: 2705 return bool(ipdf.GetNumberOfContacts()) 2706 2707 msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off") 2708 msh.metadata["ContactCells1"] = vtk2numpy( 2709 ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells") 2710 ) 2711 msh.metadata["ContactCells2"] = vtk2numpy( 2712 ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells") 2713 ) 2714 msh.properties.SetLineWidth(3) 2715 2716 msh.pipeline = OperationNode( 2717 "collide_with", 2718 parents=[self, mesh2], 2719 comment=f"#pts {msh.dataset.GetNumberOfPoints()}", 2720 ) 2721 msh.name = "SurfaceCollision" 2722 return msh
Collide this Mesh with the input surface.
Information is stored in ContactCells1
and ContactCells2
.
2724 def geodesic(self, start, end) -> Self: 2725 """ 2726 Dijkstra algorithm to compute the geodesic line. 2727 Takes as input a polygonal mesh and performs a single source shortest path calculation. 2728 2729 The output mesh contains the array "VertexIDs" that contains the ordered list of vertices 2730 traversed to get from the start vertex to the end vertex. 2731 2732 Arguments: 2733 start : (int, list) 2734 start vertex index or close point `[x,y,z]` 2735 end : (int, list) 2736 end vertex index or close point `[x,y,z]` 2737 2738 Examples: 2739 - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py) 2740 2741 ![](https://vedo.embl.es/images/advanced/geodesic.png) 2742 """ 2743 if is_sequence(start): 2744 cc = self.vertices 2745 pa = Points(cc) 2746 start = pa.closest_point(start, return_point_id=True) 2747 end = pa.closest_point(end, return_point_id=True) 2748 2749 dijkstra = vtki.new("DijkstraGraphGeodesicPath") 2750 dijkstra.SetInputData(self.dataset) 2751 dijkstra.SetStartVertex(end) # inverted in vtk 2752 dijkstra.SetEndVertex(start) 2753 dijkstra.Update() 2754 2755 weights = vtki.vtkDoubleArray() 2756 dijkstra.GetCumulativeWeights(weights) 2757 2758 idlist = dijkstra.GetIdList() 2759 ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())] 2760 2761 length = weights.GetMaxId() + 1 2762 arr = np.zeros(length) 2763 for i in range(length): 2764 arr[i] = weights.GetTuple(i)[0] 2765 2766 poly = dijkstra.GetOutput() 2767 2768 vdata = numpy2vtk(arr) 2769 vdata.SetName("CumulativeWeights") 2770 poly.GetPointData().AddArray(vdata) 2771 2772 vdata2 = numpy2vtk(ids, dtype=np.uint) 2773 vdata2.SetName("VertexIDs") 2774 poly.GetPointData().AddArray(vdata2) 2775 poly.GetPointData().Modified() 2776 2777 dmesh = Mesh(poly).copy_properties_from(self) 2778 dmesh.lw(3).alpha(1).lighting("off") 2779 dmesh.name = "GeodesicLine" 2780 2781 dmesh.pipeline = OperationNode( 2782 "GeodesicLine", 2783 parents=[self], 2784 comment=f"#steps {poly.GetNumberOfPoints()}", 2785 ) 2786 return dmesh
Dijkstra algorithm to compute the geodesic line. Takes as input a polygonal mesh and performs a single source shortest path calculation.
The output mesh contains the array "VertexIDs" that contains the ordered list of vertices traversed to get from the start vertex to the end vertex.
Arguments:
- start : (int, list)
start vertex index or close point
[x,y,z]
- end : (int, list)
end vertex index or close point
[x,y,z]
Examples:
2791 def binarize( 2792 self, 2793 values=(255, 0), 2794 spacing=None, 2795 dims=None, 2796 origin=None, 2797 ) -> "vedo.Volume": 2798 """ 2799 Convert a `Mesh` into a `Volume` where 2800 the interior voxels value is set to `values[0]` (255 by default), while 2801 the exterior voxels value is set to `values[1]` (0 by default). 2802 2803 Arguments: 2804 values : (list) 2805 background and foreground values. 2806 spacing : (list) 2807 voxel spacing in x, y and z. 2808 dims : (list) 2809 dimensions (nr. of voxels) of the output volume. 2810 origin : (list) 2811 position in space of the (0,0,0) voxel. 2812 2813 Examples: 2814 - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py) 2815 2816 ![](https://vedo.embl.es/images/volumetric/mesh2volume.png) 2817 """ 2818 assert len(values) == 2, "values must be a list of 2 values" 2819 fg_value, bg_value = values 2820 2821 bounds = self.bounds() 2822 if spacing is None: # compute spacing 2823 spacing = [0, 0, 0] 2824 diagonal = np.sqrt( 2825 (bounds[1] - bounds[0]) ** 2 2826 + (bounds[3] - bounds[2]) ** 2 2827 + (bounds[5] - bounds[4]) ** 2 2828 ) 2829 spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0 2830 2831 if dims is None: # compute dimensions 2832 dim = [0, 0, 0] 2833 for i in [0, 1, 2]: 2834 dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i])) 2835 else: 2836 dim = dims 2837 2838 white_img = vtki.vtkImageData() 2839 white_img.SetDimensions(dim) 2840 white_img.SetSpacing(spacing) 2841 white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1) 2842 2843 if origin is None: 2844 origin = [0, 0, 0] 2845 origin[0] = bounds[0] + spacing[0] 2846 origin[1] = bounds[2] + spacing[1] 2847 origin[2] = bounds[4] + spacing[2] 2848 white_img.SetOrigin(origin) 2849 2850 # if direction_matrix is not None: 2851 # white_img.SetDirectionMatrix(direction_matrix) 2852 2853 white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1) 2854 2855 # fill the image with foreground voxels: 2856 white_img.GetPointData().GetScalars().Fill(fg_value) 2857 2858 # polygonal data --> image stencil: 2859 pol2stenc = vtki.new("PolyDataToImageStencil") 2860 pol2stenc.SetInputData(self.dataset) 2861 pol2stenc.SetOutputOrigin(white_img.GetOrigin()) 2862 pol2stenc.SetOutputSpacing(white_img.GetSpacing()) 2863 pol2stenc.SetOutputWholeExtent(white_img.GetExtent()) 2864 pol2stenc.Update() 2865 2866 # cut the corresponding white image and set the background: 2867 imgstenc = vtki.new("ImageStencil") 2868 imgstenc.SetInputData(white_img) 2869 imgstenc.SetStencilConnection(pol2stenc.GetOutputPort()) 2870 # imgstenc.SetReverseStencil(True) 2871 imgstenc.SetBackgroundValue(bg_value) 2872 imgstenc.Update() 2873 2874 vol = vedo.Volume(imgstenc.GetOutput()) 2875 vol.name = "BinarizedVolume" 2876 vol.pipeline = OperationNode( 2877 "binarize", 2878 parents=[self], 2879 comment=f"dims={tuple(vol.dimensions())}", 2880 c="#e9c46a:#0096c7", 2881 ) 2882 return vol
Convert a Mesh
into a Volume
where
the interior voxels value is set to values[0]
(255 by default), while
the exterior voxels value is set to values[1]
(0 by default).
Arguments:
- values : (list) background and foreground values.
- spacing : (list) voxel spacing in x, y and z.
- dims : (list) dimensions (nr. of voxels) of the output volume.
- origin : (list) position in space of the (0,0,0) voxel.
Examples:
2884 def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume": 2885 """ 2886 Compute the `Volume` object whose voxels contains 2887 the signed distance from the mesh. 2888 2889 Arguments: 2890 bounds : (list) 2891 bounds of the output volume 2892 dims : (list) 2893 dimensions (nr. of voxels) of the output volume 2894 invert : (bool) 2895 flip the sign 2896 2897 Examples: 2898 - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py) 2899 """ 2900 if maxradius is not None: 2901 vedo.logger.warning( 2902 "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)." 2903 ) 2904 if bounds is None: 2905 bounds = self.bounds() 2906 sx = (bounds[1] - bounds[0]) / dims[0] 2907 sy = (bounds[3] - bounds[2]) / dims[1] 2908 sz = (bounds[5] - bounds[4]) / dims[2] 2909 2910 img = vtki.vtkImageData() 2911 img.SetDimensions(dims) 2912 img.SetSpacing(sx, sy, sz) 2913 img.SetOrigin(bounds[0], bounds[2], bounds[4]) 2914 img.AllocateScalars(vtki.VTK_FLOAT, 1) 2915 2916 imp = vtki.new("ImplicitPolyDataDistance") 2917 imp.SetInput(self.dataset) 2918 b2 = bounds[2] 2919 b4 = bounds[4] 2920 d0, d1, d2 = dims 2921 2922 for i in range(d0): 2923 x = i * sx + bounds[0] 2924 for j in range(d1): 2925 y = j * sy + b2 2926 for k in range(d2): 2927 v = imp.EvaluateFunction((x, y, k * sz + b4)) 2928 if invert: 2929 v = -v 2930 img.SetScalarComponentFromFloat(i, j, k, 0, v) 2931 2932 vol = vedo.Volume(img) 2933 vol.name = "SignedVolume" 2934 2935 vol.pipeline = OperationNode( 2936 "signed_distance", 2937 parents=[self], 2938 comment=f"dims={tuple(vol.dimensions())}", 2939 c="#e9c46a:#0096c7", 2940 ) 2941 return vol
Compute the Volume
object whose voxels contains
the signed distance from the mesh.
Arguments:
- bounds : (list) bounds of the output volume
- dims : (list) dimensions (nr. of voxels) of the output volume
- invert : (bool) flip the sign
Examples:
2943 def tetralize( 2944 self, 2945 side=0.02, 2946 nmax=300_000, 2947 gap=None, 2948 subsample=False, 2949 uniform=True, 2950 seed=0, 2951 debug=False, 2952 ) -> "vedo.TetMesh": 2953 """ 2954 Tetralize a closed polygonal mesh. Return a `TetMesh`. 2955 2956 Arguments: 2957 side : (float) 2958 desired side of the single tetras as fraction of the bounding box diagonal. 2959 Typical values are in the range (0.01 - 0.03) 2960 nmax : (int) 2961 maximum random numbers to be sampled in the bounding box 2962 gap : (float) 2963 keep this minimum distance from the surface, 2964 if None an automatic choice is made. 2965 subsample : (bool) 2966 subsample input surface, the geometry might be affected 2967 (the number of original faces reduceed), but higher tet quality might be obtained. 2968 uniform : (bool) 2969 generate tets more uniformly packed in the interior of the mesh 2970 seed : (int) 2971 random number generator seed 2972 debug : (bool) 2973 show an intermediate plot with sampled points 2974 2975 Examples: 2976 - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py) 2977 2978 ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg) 2979 """ 2980 surf = self.clone().clean().compute_normals() 2981 d = surf.diagonal_size() 2982 if gap is None: 2983 gap = side * d * np.sqrt(2 / 3) 2984 n = int(min((1 / side) ** 3, nmax)) 2985 2986 # fill the space w/ points 2987 x0, x1, y0, y1, z0, z1 = surf.bounds() 2988 2989 if uniform: 2990 pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42) 2991 pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100 # some small jitter 2992 else: 2993 disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2 2994 np.random.seed(seed) 2995 pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp 2996 2997 normals = surf.celldata["Normals"] 2998 cc = surf.cell_centers 2999 subpts = cc - normals * gap * 1.05 3000 pts = pts.tolist() + subpts.tolist() 3001 3002 if debug: 3003 print(".. tetralize(): subsampling and cleaning") 3004 3005 fillpts = surf.inside_points(pts) 3006 fillpts.subsample(side) 3007 3008 if gap: 3009 fillpts.distance_to(surf) 3010 fillpts.threshold("Distance", above=gap) 3011 3012 if subsample: 3013 surf.subsample(side) 3014 3015 merged_fs = vedo.merge(fillpts, surf) 3016 tmesh = merged_fs.generate_delaunay3d() 3017 tcenters = tmesh.cell_centers 3018 3019 ids = surf.inside_points(tcenters, return_ids=True) 3020 ins = np.zeros(tmesh.ncells) 3021 ins[ids] = 1 3022 3023 if debug: 3024 # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close() 3025 edges = self.edges 3026 points = self.vertices 3027 elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :]) 3028 histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d)) 3029 print(".. edges min, max", elen.min(), elen.max()) 3030 fillpts.cmap("bone") 3031 vedo.show( 3032 [ 3033 [ 3034 f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}", 3035 surf.wireframe().alpha(0.2), 3036 vedo.addons.Axes(surf), 3037 fillpts, 3038 Points(subpts).c("r4").ps(3), 3039 ], 3040 [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo], 3041 ], 3042 N=2, 3043 sharecam=False, 3044 new=True, 3045 ).close() 3046 print(".. thresholding") 3047 3048 tmesh.celldata["inside"] = ins.astype(np.uint8) 3049 tmesh.threshold("inside", above=0.9) 3050 tmesh.celldata.remove("inside") 3051 3052 if debug: 3053 print(f".. tetralize() completed, ntets = {tmesh.ncells}") 3054 3055 tmesh.pipeline = OperationNode( 3056 "tetralize", 3057 parents=[self], 3058 comment=f"#tets = {tmesh.ncells}", 3059 c="#e9c46a:#9e2a2b", 3060 ) 3061 return tmesh
Tetralize a closed polygonal mesh. Return a TetMesh
.
Arguments:
- side : (float) desired side of the single tetras as fraction of the bounding box diagonal. Typical values are in the range (0.01 - 0.03)
- nmax : (int) maximum random numbers to be sampled in the bounding box
- gap : (float) keep this minimum distance from the surface, if None an automatic choice is made.
- subsample : (bool) subsample input surface, the geometry might be affected (the number of original faces reduceed), but higher tet quality might be obtained.
- uniform : (bool) generate tets more uniformly packed in the interior of the mesh
- seed : (int) random number generator seed
- debug : (bool) show an intermediate plot with sampled points
Examples:
Inherited Members
- vedo.visual.MeshVisual
- follow_camera
- wireframe
- flat
- phong
- backface_culling
- render_lines_as_tubes
- frontface_culling
- backcolor
- bc
- linewidth
- lw
- linecolor
- lc
- texture
- vedo.pointcloud.Points
- polydata
- copy
- clone
- compute_normals_with_pca
- compute_acoplanarity
- distance_to
- clean
- subsample
- threshold
- quantize
- vertex_normals
- point_normals
- align_to
- align_to_bounding_box
- align_with_landmarks
- normalize
- mirror
- flip_normals
- add_gaussian_noise
- closest_point
- auto_distance
- hausdorff_distance
- chamfer_distance
- remove_outliers
- relax_point_positions
- smooth_mls_1d
- smooth_mls_2d
- smooth_lloyd_2d
- project_on_plane
- warp
- cut_with_plane
- cut_with_planes
- cut_with_box
- cut_with_line
- cut_with_cylinder
- cut_with_sphere
- cut_with_mesh
- cut_with_point_loop
- cut_with_scalar
- crop
- generate_surface_halo
- generate_mesh
- reconstruct_surface
- compute_clustering
- compute_connections
- compute_camera_distance
- densify
- density
- tovolume
- generate_segments
- generate_delaunay2d
- generate_voronoi
- generate_delaunay3d
- visible_points
- vedo.visual.PointsVisual
- clone2d
- copy_properties_from
- color
- c
- alpha
- lut_color_at
- opacity
- force_opaque
- force_translucent
- point_size
- ps
- render_points_as_spheres
- lighting
- point_blurring
- cellcolors
- pointcolors
- cmap
- add_trail
- update_trail
- add_shadow
- update_shadows
- labels
- labels2d
- legend
- flagpole
- flagpost
- vedo.visual.CommonVisual
- LUT
- scalar_range
- add_observer
- invoke_event
- show
- thumbnail
- pickable
- use_bounds
- draggable
- on
- off
- toggle
- add_scalarbar
- add_scalarbar3d
- vedo.core.PointAlgorithms
- apply_transform
- apply_transform_from_actor
- pos
- shift
- x
- y
- z
- rotate
- rotate_x
- rotate_y
- rotate_z
- reorient
- scale
- vedo.core.CommonAlgorithms
- pointdata
- celldata
- metadata
- memory_address
- memory_size
- modified
- box
- update_dataset
- bounds
- xbounds
- ybounds
- zbounds
- diagonal_size
- average_size
- center_of_mass
- copy_data_from
- inputdata
- npoints
- nvertices
- ncells
- points
- cell_centers
- lines
- lines_as_flat_array
- mark_boundaries
- find_cells_in_bounds
- find_cells_along_line
- find_cells_along_plane
- keep_cell_types
- map_cells_to_points
- vertices
- coordinates
- cells_as_flat_array
- cells
- cell_edge_neighbors
- map_points_to_cells
- resample_data_from
- interpolate_data_from
- add_ids
- gradient
- divergence
- vorticity
- probe
- compute_cell_size
- generate_random_data
- integrate_data
- write
- tomesh
- unsigned_distance
- smooth_data
- compute_streamlines