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