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