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