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