vedo.mesh

Submodule to work with polygonal meshes

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

Build an instance of object Mesh derived from vedo.PointCloud.

Mesh(inputobj=None, c='gold', alpha=1)
 35    def __init__(self, inputobj=None, c="gold", alpha=1):
 36        """
 37        Initialize a ``Mesh`` object.
 38
 39        Arguments:
 40            inputobj : (str, vtkPolyData, vtkActor, vedo.Mesh)
 41                If inputobj is `None` an empty mesh is created.
 42                If inputobj is a `str` then it is interpreted as the name of a file to load as mesh.
 43                If inputobj is an `vtkPolyData` or `vtkActor` or `vedo.Mesh`
 44                then a shallow copy of it is created.
 45                If inputobj is a `vedo.Mesh` then a shallow copy of it is created.
 46
 47        Examples:
 48            - [buildmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buildmesh.py)
 49            (and many others!)
 50
 51            ![](https://vedo.embl.es/images/basic/buildmesh.png)
 52        """
 53        # print("INIT MESH", super())
 54        super().__init__()
 55
 56        self.name = "Mesh"
 57
 58        if inputobj is None:
 59            # self.dataset = vtki.vtkPolyData()
 60            pass
 61
 62        elif isinstance(inputobj, vtki.vtkPolyData):
 63            self.dataset = inputobj
 64            if self.dataset.GetNumberOfCells() == 0:
 65                carr = vtki.vtkCellArray()
 66                for i in range(inputobj.GetNumberOfPoints()):
 67                    carr.InsertNextCell(1)
 68                    carr.InsertCellPoint(i)
 69                self.dataset.SetVerts(carr)
 70
 71        elif isinstance(inputobj, Mesh):
 72            self.dataset = inputobj.dataset
 73
 74        elif is_sequence(inputobj):
 75            ninp = len(inputobj)
 76            if   ninp == 4:  # assume input is [vertices, faces, lines, strips]
 77                self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2], inputobj[3])
 78            elif ninp == 3:  # assume input is [vertices, faces, lines]
 79                self.dataset = buildPolyData(inputobj[0], inputobj[1], inputobj[2])
 80            elif ninp == 2:  # assume input is [vertices, faces]
 81                self.dataset = buildPolyData(inputobj[0], inputobj[1])
 82            elif ninp == 1:  # assume input is [vertices]
 83                self.dataset = buildPolyData(inputobj[0])
 84            else:
 85                vedo.logger.error("input must be a list of max 4 elements.")
 86                raise ValueError()
 87
 88        elif isinstance(inputobj, vtki.vtkActor):
 89            self.dataset.DeepCopy(inputobj.GetMapper().GetInput())
 90            v = inputobj.GetMapper().GetScalarVisibility()
 91            self.mapper.SetScalarVisibility(v)
 92            pr = vtki.vtkProperty()
 93            pr.DeepCopy(inputobj.GetProperty())
 94            self.actor.SetProperty(pr)
 95            self.properties = pr
 96
 97        elif isinstance(inputobj, (vtki.vtkStructuredGrid, vtki.vtkRectilinearGrid)):
 98            gf = vtki.new("GeometryFilter")
 99            gf.SetInputData(inputobj)
100            gf.Update()
101            self.dataset = gf.GetOutput()
102
103        elif isinstance(inputobj, str) or "PosixPath" in str(type(inputobj)):
104            inputobj = str(inputobj)
105            self.dataset = vedo.file_io.load(inputobj).dataset
106            self.filename = inputobj
107
108        elif "meshlab" in str(type(inputobj)):
109            self.dataset = vedo.utils.meshlab2vedo(inputobj).dataset
110
111        elif "meshlib" in str(type(inputobj)):
112            import meshlib.mrmeshnumpy as mrmeshnumpy # type: ignore
113            self.dataset = buildPolyData(
114                mrmeshnumpy.getNumpyVerts(inputobj),
115                mrmeshnumpy.getNumpyFaces(inputobj.topology),
116            )
117
118        elif "trimesh" in str(type(inputobj)):
119            self.dataset = vedo.utils.trimesh2vedo(inputobj).dataset
120
121        elif "meshio" in str(type(inputobj)):
122            # self.dataset = vedo.utils.meshio2vedo(inputobj) ##TODO
123            if len(inputobj.cells) > 0:
124                mcells = []
125                for cellblock in inputobj.cells:
126                    if cellblock.type in ("triangle", "quad"):
127                        mcells += cellblock.data.tolist()
128                self.dataset = buildPolyData(inputobj.points, mcells)
129            else:
130                self.dataset = buildPolyData(inputobj.points, None)
131            # add arrays:
132            try:
133                if len(inputobj.point_data) > 0:
134                    for k in inputobj.point_data.keys():
135                        vdata = numpy2vtk(inputobj.point_data[k])
136                        vdata.SetName(str(k))
137                        self.dataset.GetPointData().AddArray(vdata)
138            except AssertionError:
139                print("Could not add meshio point data, skip.")
140
141        else:
142            try:
143                gf = vtki.new("GeometryFilter")
144                gf.SetInputData(inputobj)
145                gf.Update()
146                self.dataset = gf.GetOutput()
147            except:
148                vedo.logger.error(f"cannot build mesh from type {type(inputobj)}")
149                raise RuntimeError()
150
151        self.mapper.SetInputData(self.dataset)
152        self.actor.SetMapper(self.mapper)
153
154        self.properties.SetInterpolationToPhong()
155        self.properties.SetColor(get_color(c))
156
157        if alpha is not None:
158            self.properties.SetOpacity(alpha)
159
160        self.mapper.SetInterpolateScalarsBeforeMapping(
161            vedo.settings.interpolate_scalars_before_mapping
162        )
163
164        if vedo.settings.use_polygon_offset:
165            self.mapper.SetResolveCoincidentTopologyToPolygonOffset()
166            pof = vedo.settings.polygon_offset_factor
167            pou = vedo.settings.polygon_offset_units
168            self.mapper.SetResolveCoincidentTopologyPolygonOffsetParameters(pof, pou)
169
170        n = self.dataset.GetNumberOfPoints()
171        self.pipeline = OperationNode(self, comment=f"#pts {n}")

Initialize a Mesh object.

Arguments:
  • inputobj : (str, vtkPolyData, vtkActor, Mesh) If inputobj is None an empty mesh is created. If inputobj is a str then it is interpreted as the name of a file to load as mesh. If inputobj is an vtkPolyData or vtkActor or Mesh then a shallow copy of it is created. If inputobj is a Mesh then a shallow copy of it is created.
Examples:

edges
251    @property
252    def edges(self):
253        """Return an array containing the edges connectivity."""
254        extractEdges = vtki.new("ExtractEdges")
255        extractEdges.SetInputData(self.dataset)
256        # eed.UseAllPointsOn()
257        extractEdges.Update()
258        lpoly = extractEdges.GetOutput()
259
260        arr1d = vtk2numpy(lpoly.GetLines().GetData())
261        # [nids1, id0 ... idn, niids2, id0 ... idm,  etc].
262
263        i = 0
264        conn = []
265        n = len(arr1d)
266        for _ in range(n):
267            cell = [arr1d[i + k + 1] for k in range(arr1d[i])]
268            conn.append(cell)
269            i += arr1d[i] + 1
270            if i >= n:
271                break
272        return conn  # cannot always make a numpy array of it!

Return an array containing the edges connectivity.

vertex_normals: numpy.ndarray
274    @property 
275    def vertex_normals(self) -> np.ndarray:
276        """
277        Retrieve vertex normals as a numpy array. 
278        If needed, normals are automatically computed via `compute_normals()`.
279        Check out also `compute_normals_with_pca()`.
280        """
281        vtknormals = self.dataset.GetPointData().GetNormals()
282        if vtknormals is None:
283            self.compute_normals()
284            vtknormals = self.dataset.GetPointData().GetNormals()
285        return vtk2numpy(vtknormals)

Retrieve vertex normals as a numpy array. If needed, normals are automatically computed via compute_normals(). Check out also compute_normals_with_pca().

cell_normals: numpy.ndarray
287    @property
288    def cell_normals(self) -> np.ndarray:
289        """
290        Retrieve face normals as a numpy array.
291        If need be normals are computed via `compute_normals()`.
292        Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`.
293        """
294        vtknormals = self.dataset.GetCellData().GetNormals()
295        if vtknormals is None:
296            self.compute_normals()
297            vtknormals = self.dataset.GetCellData().GetNormals()
298        return vtk2numpy(vtknormals)

Retrieve face normals as a numpy array. If need be normals are computed via compute_normals(). Check out also compute_normals(cells=True) and compute_normals_with_pca().

def compute_normals( self, points=True, cells=True, feature_angle=None, consistency=True) -> Self:
300    def compute_normals(self, points=True, cells=True, feature_angle=None, consistency=True) -> Self:
301        """
302        Compute cell and vertex normals for the mesh.
303
304        Arguments:
305            points : (bool)
306                do the computation for the vertices too
307            cells : (bool)
308                do the computation for the cells too
309            feature_angle : (float)
310                specify the angle that defines a sharp edge.
311                If the difference in angle across neighboring polygons is greater than this value,
312                the shared edge is considered "sharp" and it is split.
313            consistency : (bool)
314                turn on/off the enforcement of consistent polygon ordering.
315
316        .. warning::
317            If `feature_angle` is set then the Mesh can be modified, and it
318            can have a different number of vertices from the original.
319
320            Note that the appearance of the mesh may change if the normals are computed,
321            as shading is automatically enabled when such information is present.
322            Use `mesh.flat()` to avoid smoothing effects.
323        """
324        pdnorm = vtki.new("PolyDataNormals")
325        pdnorm.SetInputData(self.dataset)
326        pdnorm.SetComputePointNormals(points)
327        pdnorm.SetComputeCellNormals(cells)
328        pdnorm.SetConsistency(consistency)
329        pdnorm.FlipNormalsOff()
330        if feature_angle:
331            pdnorm.SetSplitting(True)
332            pdnorm.SetFeatureAngle(feature_angle)
333        else:
334            pdnorm.SetSplitting(False)
335        pdnorm.Update()
336        out = pdnorm.GetOutput()
337        self._update(out, reset_locators=False)
338        return self

Compute cell and vertex normals for the mesh.

Arguments:
  • points : (bool) do the computation for the vertices too
  • cells : (bool) do the computation for the cells too
  • feature_angle : (float) specify the angle that defines a sharp edge. If the difference in angle across neighboring polygons is greater than this value, the shared edge is considered "sharp" and it is split.
  • consistency : (bool) turn on/off the enforcement of consistent polygon ordering.

If feature_angle is set then the Mesh can be modified, and it can have a different number of vertices from the original.

Note that the appearance of the mesh may change if the normals are computed, as shading is automatically enabled when such information is present. Use mesh.flat() to avoid smoothing effects.

def reverse(self, cells=True, normals=False) -> Self:
340    def reverse(self, cells=True, normals=False) -> Self:
341        """
342        Reverse the order of polygonal cells
343        and/or reverse the direction of point and cell normals.
344
345        Two flags are used to control these operations:
346            - `cells=True` reverses the order of the indices in the cell connectivity list.
347                If cell is a list of IDs only those cells will be reversed.
348            - `normals=True` reverses the normals by multiplying the normal vector by -1
349                (both point and cell normals, if present).
350        """
351        poly = self.dataset
352
353        if is_sequence(cells):
354            for cell in cells:
355                poly.ReverseCell(cell)
356            poly.GetCellData().Modified()
357            return self  ##############
358
359        rev = vtki.new("ReverseSense")
360        if cells:
361            rev.ReverseCellsOn()
362        else:
363            rev.ReverseCellsOff()
364        if normals:
365            rev.ReverseNormalsOn()
366        else:
367            rev.ReverseNormalsOff()
368        rev.SetInputData(poly)
369        rev.Update()
370        self._update(rev.GetOutput(), reset_locators=False)
371        self.pipeline = OperationNode("reverse", parents=[self])
372        return self

Reverse the order of polygonal cells and/or reverse the direction of point and cell normals.

Two flags are used to control these operations:
  • cells=True reverses the order of the indices in the cell connectivity list. If cell is a list of IDs only those cells will be reversed.
  • normals=True reverses the normals by multiplying the normal vector by -1 (both point and cell normals, if present).
def volume(self) -> float:
374    def volume(self) -> float:
375        """
376        Compute the volume occupied by mesh.
377        The mesh must be triangular for this to work.
378        To triangulate a mesh use `mesh.triangulate()`.
379        """
380        mass = vtki.new("MassProperties")
381        mass.SetGlobalWarningDisplay(0)
382        mass.SetInputData(self.dataset)
383        mass.Update()
384        mass.SetGlobalWarningDisplay(1)
385        return mass.GetVolume()

Compute the volume occupied by mesh. The mesh must be triangular for this to work. To triangulate a mesh use mesh.triangulate().

def area(self) -> float:
387    def area(self) -> float:
388        """
389        Compute the surface area of the mesh.
390        The mesh must be triangular for this to work.
391        To triangulate a mesh use `mesh.triangulate()`.
392        """
393        mass = vtki.new("MassProperties")
394        mass.SetGlobalWarningDisplay(0)
395        mass.SetInputData(self.dataset)
396        mass.Update()
397        mass.SetGlobalWarningDisplay(1)
398        return mass.GetSurfaceArea()

Compute the surface area of the mesh. The mesh must be triangular for this to work. To triangulate a mesh use mesh.triangulate().

def is_closed(self) -> bool:
400    def is_closed(self) -> bool:
401        """
402        Return `True` if the mesh is watertight.
403        Note that if the mesh contains coincident points the result may be flase.
404        Use in this case `mesh.clean()` to merge coincident points.
405        """
406        fe = vtki.new("FeatureEdges")
407        fe.BoundaryEdgesOn()
408        fe.FeatureEdgesOff()
409        fe.NonManifoldEdgesOn()
410        fe.SetInputData(self.dataset)
411        fe.Update()
412        ne = fe.GetOutput().GetNumberOfCells()
413        return not bool(ne)

Return True if the mesh is watertight. Note that if the mesh contains coincident points the result may be flase. Use in this case mesh.clean() to merge coincident points.

def is_manifold(self) -> bool:
415    def is_manifold(self) -> bool:
416        """Return `True` if the mesh is manifold."""
417        fe = vtki.new("FeatureEdges")
418        fe.BoundaryEdgesOff()
419        fe.FeatureEdgesOff()
420        fe.NonManifoldEdgesOn()
421        fe.SetInputData(self.dataset)
422        fe.Update()
423        ne = fe.GetOutput().GetNumberOfCells()
424        return not bool(ne)

Return True if the mesh is manifold.

def non_manifold_faces(self, remove=True, tol='auto') -> Self:
426    def non_manifold_faces(self, remove=True, tol="auto") -> Self:
427        """
428        Detect and (try to) remove non-manifold faces of a triangular mesh:
429
430            - set `remove` to `False` to mark cells without removing them.
431            - set `tol=0` for zero-tolerance, the result will be manifold but with holes.
432            - set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
433            - set `tol="auto"` to make an automatic choice of the tolerance.
434        """
435        # mark original point and cell ids
436        self.add_ids()
437        toremove = self.boundaries(
438            boundary_edges=False,
439            non_manifold_edges=True,
440            cell_edge=True,
441            return_cell_ids=True,
442        )
443        if len(toremove) == 0: # type: ignore
444            return self
445
446        points = self.coordinates
447        faces = self.cells
448        centers = self.cell_centers().coordinates
449
450        copy = self.clone()
451        copy.delete_cells(toremove).clean()
452        copy.compute_normals(cells=False)
453        normals = copy.vertex_normals
454        deltas, deltas_i = [], []
455
456        for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"):
457            pids = copy.closest_point(centers[i], n=3, return_point_id=True)
458            norms = normals[pids]
459            n = np.mean(norms, axis=0)
460            dn = np.linalg.norm(n)
461            if not dn:
462                continue
463            n = n / dn
464
465            p0, p1, p2 = points[faces[i]][:3]
466            v = np.cross(p1 - p0, p2 - p0)
467            lv = np.linalg.norm(v)
468            if not lv:
469                continue
470            v = v / lv
471
472            cosa = 1 - np.dot(n, v)
473            deltas.append(cosa)
474            deltas_i.append(i)
475
476        recover = []
477        if len(deltas) > 0:
478            mean_delta = np.mean(deltas)
479            err_delta = np.std(deltas)
480            txt = ""
481            if tol == "auto":  # automatic choice
482                tol = mean_delta / 5
483                txt = f"\n Automatic tol. : {tol: .4f}"
484            for i, cosa in zip(deltas_i, deltas):
485                if cosa < tol:
486                    recover.append(i)
487
488            vedo.logger.info(
489                f"\n --------- Non manifold faces ---------"
490                f"\n Average tol.   : {mean_delta: .4f} +- {err_delta: .4f}{txt}"
491                f"\n Removed faces  : {len(toremove)}" # type: ignore
492                f"\n Recovered faces: {len(recover)}"
493            )
494
495        toremove = list(set(toremove) - set(recover)) # type: ignore
496
497        if not remove:
498            mark = np.zeros(self.ncells, dtype=np.uint8)
499            mark[recover] = 1
500            mark[toremove] = 2
501            self.celldata["NonManifoldCell"] = mark
502        else:
503            self.delete_cells(toremove) # type: ignore
504
505        self.pipeline = OperationNode(
506            "non_manifold_faces",
507            parents=[self],
508            comment=f"#cells {self.dataset.GetNumberOfCells()}",
509        )
510        return self

Detect and (try to) remove non-manifold faces of a triangular mesh:

- set `remove` to `False` to mark cells without removing them.
- set `tol=0` for zero-tolerance, the result will be manifold but with holes.
- set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
- set `tol="auto"` to make an automatic choice of the tolerance.
def euler_characteristic(self) -> int:
513    def euler_characteristic(self) -> int:
514        """
515        Compute the Euler characteristic of the mesh.
516        The Euler characteristic is a topological invariant for surfaces.
517        """
518        return self.npoints - len(self.edges) + self.ncells

Compute the Euler characteristic of the mesh. The Euler characteristic is a topological invariant for surfaces.

def genus(self) -> int:
520    def genus(self) -> int:
521        """
522        Compute the genus of the mesh.
523        The genus is a topological invariant for surfaces.
524        """
525        nb = len(self.boundaries().split()) - 1
526        return (2 - self.euler_characteristic() - nb ) / 2

Compute the genus of the mesh. The genus is a topological invariant for surfaces.

def to_reeb_graph(self, field_id=0):
528    def to_reeb_graph(self, field_id=0):
529        """
530        Convert the mesh into a Reeb graph.
531        The Reeb graph is a topological structure that captures the evolution
532        of the level sets of a scalar field.
533
534        Arguments:
535            field_id : (int)
536                the id of the scalar field to use.
537
538        Example:
539            ```python
540            from vedo import *
541            mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl")
542            mesh.rotate_x(10).rotate_y(15).alpha(0.5)
543            mesh.pointdata["scalars"] = mesh.coordinates[:, 2]
544
545            printc("is_closed  :", mesh.is_closed())
546            printc("is_manifold:", mesh.is_manifold())
547            printc("euler_char :", mesh.euler_characteristic())
548            printc("genus      :", mesh.genus())
549
550            reeb = mesh.to_reeb_graph()
551            ids = reeb[0].pointdata["Vertex Ids"]
552            pts = Points(mesh.coordinates[ids], r=10)
553
554            show([[mesh, pts], reeb], N=2, sharecam=False)
555            ```
556        """
557        rg = vtki.new("PolyDataToReebGraphFilter")
558        rg.SetInputData(self.dataset)
559        rg.SetFieldId(field_id)
560        rg.Update()
561        gr = vedo.pyplot.DirectedGraph()
562        gr.mdg = rg.GetOutput()
563        gr.build()
564        return gr

Convert the mesh into a Reeb graph. The Reeb graph is a topological structure that captures the evolution of the level sets of a scalar field.

Arguments:
  • field_id : (int) the id of the scalar field to use.
Example:
from vedo import *
mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl")
mesh.rotate_x(10).rotate_y(15).alpha(0.5)
mesh.pointdata["scalars"] = mesh.coordinates[:, 2]

printc("is_closed  :", mesh.is_closed())
printc("is_manifold:", mesh.is_manifold())
printc("euler_char :", mesh.euler_characteristic())
printc("genus      :", mesh.genus())

reeb = mesh.to_reeb_graph()
ids = reeb[0].pointdata["Vertex Ids"]
pts = Points(mesh.coordinates[ids], r=10)

show([[mesh, pts], reeb], N=2, sharecam=False)
def shrink(self, fraction=0.85) -> Self:
567    def shrink(self, fraction=0.85) -> Self:
568        """
569        Shrink the triangle polydata in the representation of the input mesh.
570
571        Examples:
572            - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py)
573
574            ![](https://vedo.embl.es/images/basic/shrink.png)
575        """
576        # Overriding base class method core.shrink()
577        shrink = vtki.new("ShrinkPolyData")
578        shrink.SetInputData(self.dataset)
579        shrink.SetShrinkFactor(fraction)
580        shrink.Update()
581        self._update(shrink.GetOutput())
582        self.pipeline = OperationNode("shrink", parents=[self])
583        return self

Shrink the triangle polydata in the representation of the input mesh.

Examples:

def cap(self, return_cap=False) -> Self:
585    def cap(self, return_cap=False) -> Self:
586        """
587        Generate a "cap" on a clipped mesh, or caps sharp edges.
588
589        Examples:
590            - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py)
591
592            ![](https://vedo.embl.es/images/advanced/cutAndCap.png)
593
594        See also: `join()`, `join_segments()`, `slice()`.
595        """
596        fe = vtki.new("FeatureEdges")
597        fe.SetInputData(self.dataset)
598        fe.BoundaryEdgesOn()
599        fe.FeatureEdgesOff()
600        fe.NonManifoldEdgesOff()
601        fe.ManifoldEdgesOff()
602        fe.Update()
603
604        stripper = vtki.new("Stripper")
605        stripper.SetInputData(fe.GetOutput())
606        stripper.JoinContiguousSegmentsOn()
607        stripper.Update()
608
609        boundary_poly = vtki.vtkPolyData()
610        boundary_poly.SetPoints(stripper.GetOutput().GetPoints())
611        boundary_poly.SetPolys(stripper.GetOutput().GetLines())
612
613        rev = vtki.new("ReverseSense")
614        rev.ReverseCellsOn()
615        rev.SetInputData(boundary_poly)
616        rev.Update()
617
618        tf = vtki.new("TriangleFilter")
619        tf.SetInputData(rev.GetOutput())
620        tf.Update()
621
622        if return_cap:
623            m = Mesh(tf.GetOutput())
624            m.pipeline = OperationNode(
625                "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}"
626            )
627            m.name = "MeshCap"
628            return m
629
630        polyapp = vtki.new("AppendPolyData")
631        polyapp.AddInputData(self.dataset)
632        polyapp.AddInputData(tf.GetOutput())
633        polyapp.Update()
634
635        self._update(polyapp.GetOutput())
636        self.clean()
637
638        self.pipeline = OperationNode(
639            "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
640        )
641        return self

Generate a "cap" on a clipped mesh, or caps sharp edges.

Examples:

See also: join(), join_segments(), slice().

def join(self, polys=True, reset=False) -> Self:
643    def join(self, polys=True, reset=False) -> Self:
644        """
645        Generate triangle strips and/or polylines from
646        input polygons, triangle strips, and lines.
647
648        Input polygons are assembled into triangle strips only if they are triangles;
649        other types of polygons are passed through to the output and not stripped.
650        Use mesh.triangulate() to triangulate non-triangular polygons prior to running
651        this filter if you need to strip all the data.
652
653        Also note that if triangle strips or polylines are present in the input
654        they are passed through and not joined nor extended.
655        If you wish to strip these use mesh.triangulate() to fragment the input
656        into triangles and lines prior to applying join().
657
658        Arguments:
659            polys : (bool)
660                polygonal segments will be joined if they are contiguous
661            reset : (bool)
662                reset points ordering
663
664        Warning:
665            If triangle strips or polylines exist in the input data
666            they will be passed through to the output data.
667            This filter will only construct triangle strips if triangle polygons
668            are available; and will only construct polylines if lines are available.
669
670        Example:
671            ```python
672            from vedo import *
673            c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate()
674            c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate()
675            intersect = c1.intersect_with(c2).join(reset=True)
676            spline = Spline(intersect).c('blue').lw(5)
677            show(c1, c2, spline, intersect.labels('id'), axes=1).close()
678            ```
679            ![](https://vedo.embl.es/images/feats/line_join.png)
680        """
681        sf = vtki.new("Stripper")
682        sf.SetPassThroughCellIds(True)
683        sf.SetPassThroughPointIds(True)
684        sf.SetJoinContiguousSegments(polys)
685        sf.SetInputData(self.dataset)
686        sf.Update()
687        if reset:
688            poly = sf.GetOutput()
689            cpd = vtki.new("CleanPolyData")
690            cpd.PointMergingOn()
691            cpd.ConvertLinesToPointsOn()
692            cpd.ConvertPolysToLinesOn()
693            cpd.ConvertStripsToPolysOn()
694            cpd.SetInputData(poly)
695            cpd.Update()
696            poly = cpd.GetOutput()
697            vpts = poly.GetCell(0).GetPoints().GetData()
698            poly.GetPoints().SetData(vpts)
699        else:
700            poly = sf.GetOutput()
701
702        self._update(poly)
703
704        self.pipeline = OperationNode(
705            "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
706        )
707        return self

Generate triangle strips and/or polylines from input polygons, triangle strips, and lines.

Input polygons are assembled into triangle strips only if they are triangles; other types of polygons are passed through to the output and not stripped. Use mesh.triangulate() to triangulate non-triangular polygons prior to running this filter if you need to strip all the data.

Also note that if triangle strips or polylines are present in the input they are passed through and not joined nor extended. If you wish to strip these use mesh.triangulate() to fragment the input into triangles and lines prior to applying join().

Arguments:
  • polys : (bool) polygonal segments will be joined if they are contiguous
  • reset : (bool) reset points ordering
Warning:

If triangle strips or polylines exist in the input data they will be passed through to the output data. This filter will only construct triangle strips if triangle polygons are available; and will only construct polylines if lines are available.

Example:
from vedo import *
c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate()
c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate()
intersect = c1.intersect_with(c2).join(reset=True)
spline = Spline(intersect).c('blue').lw(5)
show(c1, c2, spline, intersect.labels('id'), axes=1).close()

def join_segments(self, closed=True, tol=0.001) -> list:
709    def join_segments(self, closed=True, tol=1e-03) -> list:
710        """
711        Join line segments into contiguous lines.
712        Useful to call with `triangulate()` method.
713
714        Returns:
715            list of `shapes.Lines`
716
717        Example:
718            ```python
719            from vedo import *
720            msh = Torus().alpha(0.1).wireframe()
721            intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5')
722            slices = [s.triangulate() for s in intersection.join_segments()]
723            show(msh, intersection, merge(slices), axes=1, viewup='z')
724            ```
725            ![](https://vedo.embl.es/images/feats/join_segments.jpg)
726        """
727        vlines = []
728        for _ipiece, outline in enumerate(self.split(must_share_edge=False)):
729
730            outline.clean()
731            pts = outline.coordinates
732            if len(pts) < 3:
733                continue
734            avesize = outline.average_size()
735            lines = outline.lines
736            # print("---lines", lines, "in piece", _ipiece)
737            tol = avesize / pts.shape[0] * tol
738
739            k = 0
740            joinedpts = [pts[k]]
741            for _ in range(len(pts)):
742                pk = pts[k]
743                for j, line in enumerate(lines):
744
745                    id0, id1 = line[0], line[-1]
746                    p0, p1 = pts[id0], pts[id1]
747
748                    if np.linalg.norm(p0 - pk) < tol:
749                        n = len(line)
750                        for m in range(1, n):
751                            joinedpts.append(pts[line[m]])
752                        # joinedpts.append(p1)
753                        k = id1
754                        lines.pop(j)
755                        break
756
757                    if np.linalg.norm(p1 - pk) < tol:
758                        n = len(line)
759                        for m in reversed(range(0, n - 1)):
760                            joinedpts.append(pts[line[m]])
761                        # joinedpts.append(p0)
762                        k = id0
763                        lines.pop(j)
764                        break
765
766            if len(joinedpts) > 1:
767                newline = vedo.shapes.Line(joinedpts, closed=closed)
768                newline.clean()
769                newline.actor.SetProperty(self.properties)
770                newline.properties = self.properties
771                newline.pipeline = OperationNode(
772                    "join_segments",
773                    parents=[self],
774                    comment=f"#pts {newline.dataset.GetNumberOfPoints()}",
775                )
776                vlines.append(newline)
777
778        return vlines

Join line segments into contiguous lines. Useful to call with triangulate() method.

Returns:

list of shapes.Lines

Example:
from vedo import *
msh = Torus().alpha(0.1).wireframe()
intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5')
slices = [s.triangulate() for s in intersection.join_segments()]
show(msh, intersection, merge(slices), axes=1, viewup='z')

def join_with_strips(self, b1, closed=True) -> Self:
780    def join_with_strips(self, b1, closed=True) -> Self:
781        """
782        Join booundary lines by creating a triangle strip between them.
783
784        Example:
785        ```python
786        from vedo import *
787        m1 = Cylinder(cap=False).boundaries()
788        m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
789        strips = m1.join_with_strips(m2)
790        show(m1, m2, strips, axes=1).close()
791        ```
792        """
793        b0 = self.clone().join()
794        b1 = b1.clone().join()
795
796        vertices0 = b0.vertices.tolist()
797        vertices1 = b1.vertices.tolist()
798
799        lines0 = b0.lines
800        lines1 = b1.lines
801        m =  len(lines0)
802        assert m == len(lines1), (
803            "lines must have the same number of points\n"
804            f"line has {m} points in b0 and {len(lines1)} in b1"
805        )
806
807        strips = []
808        points: List[Any] = []
809
810        for j in range(m):
811
812            ids0j = list(lines0[j])
813            ids1j = list(lines1[j])
814
815            n = len(ids0j)
816            assert n == len(ids1j), (
817                "lines must have the same number of points\n"
818                f"line {j} has {n} points in b0 and {len(ids1j)} in b1"
819            )
820
821            if closed:
822                ids0j.append(ids0j[0])
823                ids1j.append(ids1j[0])
824                vertices0.append(vertices0[ids0j[0]])
825                vertices1.append(vertices1[ids1j[0]])
826                n = n + 1
827
828            strip = []  # create a triangle strip
829            npt = len(points)
830            for ipt in range(n):
831                points.append(vertices0[ids0j[ipt]])
832                points.append(vertices1[ids1j[ipt]])
833
834            strip = list(range(npt, npt + 2*n))
835            strips.append(strip)
836
837        return Mesh([points, [], [], strips], c="k6")

Join booundary lines by creating a triangle strip between them.

Example:

from vedo import *
m1 = Cylinder(cap=False).boundaries()
m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
strips = m1.join_with_strips(m2)
show(m1, m2, strips, axes=1).close()
def split_polylines(self) -> Self:
839    def split_polylines(self) -> Self:
840        """Split polylines into separate segments."""
841        tf = vtki.new("TriangleFilter")
842        tf.SetPassLines(True)
843        tf.SetPassVerts(False)
844        tf.SetInputData(self.dataset)
845        tf.Update()
846        self._update(tf.GetOutput(), reset_locators=False)
847        self.lw(0).lighting("default").pickable()
848        self.pipeline = OperationNode(
849            "split_polylines", parents=[self],
850            comment=f"#lines {self.dataset.GetNumberOfLines()}"
851        )
852        return self

Split polylines into separate segments.

def remove_all_lines(self) -> Self:
854    def remove_all_lines(self) -> Self:
855        """Remove all line elements from the mesh."""
856        self.dataset.GetLines().Reset()
857        return self

Remove all line elements from the mesh.

def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
859    def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
860        """
861        Slice a mesh with a plane and fill the contour.
862
863        Example:
864            ```python
865            from vedo import *
866            msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe()
867            mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0])
868            mslice.c('purple5')
869            show(msh, mslice, axes=1)
870            ```
871            ![](https://vedo.embl.es/images/feats/mesh_slice.jpg)
872
873        See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`.
874        """
875        intersection = self.intersect_with_plane(origin=origin, normal=normal)
876        slices = [s.triangulate() for s in intersection.join_segments()]
877        mslices = vedo.pointcloud.merge(slices)
878        if mslices:
879            mslices.name = "MeshSlice"
880            mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}")
881        return mslices

Slice a mesh with a plane and fill the contour.

Example:
from vedo import *
msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe()
mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0])
mslice.c('purple5')
show(msh, mslice, axes=1)

See also: join(), join_segments(), cap(), cut_with_plane().

def triangulate(self, verts=True, lines=True) -> Self:
883    def triangulate(self, verts=True, lines=True) -> Self:
884        """
885        Converts mesh polygons into triangles.
886
887        If the input mesh is only made of 2D lines (no faces) the output will be a triangulation
888        that fills the internal area. The contours may be concave, and may even contain holes,
889        i.e. a contour may contain an internal contour winding in the opposite
890        direction to indicate that it is a hole.
891
892        Arguments:
893            verts : (bool)
894                if True, break input vertex cells into individual vertex cells (one point per cell).
895                If False, the input vertex cells will be ignored.
896            lines : (bool)
897                if True, break input polylines into line segments.
898                If False, input lines will be ignored and the output will have no lines.
899        """
900        if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips():
901            # print("Using vtkTriangleFilter")
902            tf = vtki.new("TriangleFilter")
903            tf.SetPassLines(lines)
904            tf.SetPassVerts(verts)
905
906        elif self.dataset.GetNumberOfLines():
907            # print("Using vtkContourTriangulator")
908            tf = vtki.new("ContourTriangulator")
909            tf.TriangulationErrorDisplayOn()
910
911        else:
912            vedo.logger.debug("input in triangulate() seems to be void! Skip.")
913            return self
914
915        tf.SetInputData(self.dataset)
916        tf.Update()
917        self._update(tf.GetOutput(), reset_locators=False)
918        self.lw(0).lighting("default").pickable()
919
920        self.pipeline = OperationNode(
921            "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}"
922        )
923        return self

Converts mesh polygons into triangles.

If the input mesh is only made of 2D lines (no faces) the output will be a triangulation that fills the internal area. The contours may be concave, and may even contain holes, i.e. a contour may contain an internal contour winding in the opposite direction to indicate that it is a hole.

Arguments:
  • verts : (bool) if True, break input vertex cells into individual vertex cells (one point per cell). If False, the input vertex cells will be ignored.
  • lines : (bool) if True, break input polylines into line segments. If False, input lines will be ignored and the output will have no lines.
def compute_cell_vertex_count(self) -> Self:
925    def compute_cell_vertex_count(self) -> Self:
926        """
927        Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.
928        """
929        csf = vtki.new("CellSizeFilter")
930        csf.SetInputData(self.dataset)
931        csf.SetComputeArea(False)
932        csf.SetComputeVolume(False)
933        csf.SetComputeLength(False)
934        csf.SetComputeVertexCount(True)
935        csf.SetVertexCountArrayName("VertexCount")
936        csf.Update()
937        self.dataset.GetCellData().AddArray(
938            csf.GetOutput().GetCellData().GetArray("VertexCount")
939        )
940        return self

Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.

def compute_quality(self, metric=6) -> Self:
942    def compute_quality(self, metric=6) -> Self:
943        """
944        Calculate metrics of quality for the elements of a triangular mesh.
945        This method adds to the mesh a cell array named "Quality".
946        See class
947        [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html).
948
949        Arguments:
950            metric : (int)
951                type of available estimators are:
952                - EDGE RATIO, 0
953                - ASPECT RATIO, 1
954                - RADIUS RATIO, 2
955                - ASPECT FROBENIUS, 3
956                - MED ASPECT FROBENIUS, 4
957                - MAX ASPECT FROBENIUS, 5
958                - MIN_ANGLE, 6
959                - COLLAPSE RATIO, 7
960                - MAX ANGLE, 8
961                - CONDITION, 9
962                - SCALED JACOBIAN, 10
963                - SHEAR, 11
964                - RELATIVE SIZE SQUARED, 12
965                - SHAPE, 13
966                - SHAPE AND SIZE, 14
967                - DISTORTION, 15
968                - MAX EDGE RATIO, 16
969                - SKEW, 17
970                - TAPER, 18
971                - VOLUME, 19
972                - STRETCH, 20
973                - DIAGONAL, 21
974                - DIMENSION, 22
975                - ODDY, 23
976                - SHEAR AND SIZE, 24
977                - JACOBIAN, 25
978                - WARPAGE, 26
979                - ASPECT GAMMA, 27
980                - AREA, 28
981                - ASPECT BETA, 29
982
983        Examples:
984            - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py)
985
986            ![](https://vedo.embl.es/images/advanced/meshquality.png)
987        """
988        qf = vtki.new("MeshQuality")
989        qf.SetInputData(self.dataset)
990        qf.SetTriangleQualityMeasure(metric)
991        qf.SaveCellQualityOn()
992        qf.Update()
993        self._update(qf.GetOutput(), reset_locators=False)
994        self.mapper.SetScalarModeToUseCellData()
995        self.pipeline = OperationNode("compute_quality", parents=[self])
996        return self

Calculate metrics of quality for the elements of a triangular mesh. This method adds to the mesh a cell array named "Quality". See class vtkMeshQuality.

Arguments:
  • metric : (int) type of available estimators are:
    • EDGE RATIO, 0
    • ASPECT RATIO, 1
    • RADIUS RATIO, 2
    • ASPECT FROBENIUS, 3
    • MED ASPECT FROBENIUS, 4
    • MAX ASPECT FROBENIUS, 5
    • MIN_ANGLE, 6
    • COLLAPSE RATIO, 7
    • MAX ANGLE, 8
    • CONDITION, 9
    • SCALED JACOBIAN, 10
    • SHEAR, 11
    • RELATIVE SIZE SQUARED, 12
    • SHAPE, 13
    • SHAPE AND SIZE, 14
    • DISTORTION, 15
    • MAX EDGE RATIO, 16
    • SKEW, 17
    • TAPER, 18
    • VOLUME, 19
    • STRETCH, 20
    • DIAGONAL, 21
    • DIMENSION, 22
    • ODDY, 23
    • SHEAR AND SIZE, 24
    • JACOBIAN, 25
    • WARPAGE, 26
    • ASPECT GAMMA, 27
    • AREA, 28
    • ASPECT BETA, 29
Examples:

def count_vertices(self) -> numpy.ndarray:
 998    def count_vertices(self) -> np.ndarray:
 999        """Count the number of vertices each cell has and return it as a numpy array"""
1000        vc = vtki.new("CountVertices")
1001        vc.SetInputData(self.dataset)
1002        vc.SetOutputArrayName("VertexCount")
1003        vc.Update()
1004        varr = vc.GetOutput().GetCellData().GetArray("VertexCount")
1005        return vtk2numpy(varr)

Count the number of vertices each cell has and return it as a numpy array

def check_validity(self, tol=0) -> numpy.ndarray:
1007    def check_validity(self, tol=0) -> np.ndarray:
1008        """
1009        Return a numpy array of possible problematic faces following this convention:
1010        - Valid               =  0
1011        - WrongNumberOfPoints =  1
1012        - IntersectingEdges   =  2
1013        - IntersectingFaces   =  4
1014        - NoncontiguousEdges  =  8
1015        - Nonconvex           = 10
1016        - OrientedIncorrectly = 20
1017
1018        Arguments:
1019            tol : (float)
1020                value is used as an epsilon for floating point
1021                equality checks throughout the cell checking process.
1022        """
1023        vald = vtki.new("CellValidator")
1024        if tol:
1025            vald.SetTolerance(tol)
1026        vald.SetInputData(self.dataset)
1027        vald.Update()
1028        varr = vald.GetOutput().GetCellData().GetArray("ValidityState")
1029        return vtk2numpy(varr)

Return a numpy array of possible problematic faces following this convention:

  • Valid = 0
  • WrongNumberOfPoints = 1
  • IntersectingEdges = 2
  • IntersectingFaces = 4
  • NoncontiguousEdges = 8
  • Nonconvex = 10
  • OrientedIncorrectly = 20
Arguments:
  • tol : (float) value is used as an epsilon for floating point equality checks throughout the cell checking process.
def compute_curvature(self, method=0) -> Self:
1031    def compute_curvature(self, method=0) -> Self:
1032        """
1033        Add scalars to `Mesh` that contains the curvature calculated in three different ways.
1034
1035        Variable `method` can be:
1036        - 0 = gaussian
1037        - 1 = mean curvature
1038        - 2 = max curvature
1039        - 3 = min curvature
1040
1041        Example:
1042            ```python
1043            from vedo import Torus
1044            Torus().compute_curvature().add_scalarbar().show().close()
1045            ```
1046            ![](https://vedo.embl.es/images/advanced/torus_curv.png)
1047        """
1048        curve = vtki.new("Curvatures")
1049        curve.SetInputData(self.dataset)
1050        curve.SetCurvatureType(method)
1051        curve.Update()
1052        self._update(curve.GetOutput(), reset_locators=False)
1053        self.mapper.ScalarVisibilityOn()
1054        return self

Add scalars to Mesh that contains the curvature calculated in three different ways.

Variable method can be:

  • 0 = gaussian
  • 1 = mean curvature
  • 2 = max curvature
  • 3 = min curvature
Example:
from vedo import Torus
Torus().compute_curvature().add_scalarbar().show().close()

def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self:
1056    def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self:
1057        """
1058        Add to `Mesh` a scalar array that contains distance along a specified direction.
1059
1060        Arguments:
1061            low : (list)
1062                one end of the line (small scalar values)
1063            high : (list)
1064                other end of the line (large scalar values)
1065            vrange : (list)
1066                set the range of the scalar
1067
1068        Example:
1069            ```python
1070            from vedo import Sphere
1071            s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1))
1072            s.add_scalarbar().show(axes=1).close()
1073            ```
1074            ![](https://vedo.embl.es/images/basic/compute_elevation.png)
1075        """
1076        ef = vtki.new("ElevationFilter")
1077        ef.SetInputData(self.dataset)
1078        ef.SetLowPoint(low)
1079        ef.SetHighPoint(high)
1080        ef.SetScalarRange(vrange)
1081        ef.Update()
1082        self._update(ef.GetOutput(), reset_locators=False)
1083        self.mapper.ScalarVisibilityOn()
1084        return self

Add to Mesh a scalar array that contains distance along a specified direction.

Arguments:
  • low : (list) one end of the line (small scalar values)
  • high : (list) other end of the line (large scalar values)
  • vrange : (list) set the range of the scalar
Example:
from vedo import Sphere
s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1))
s.add_scalarbar().show(axes=1).close()

def laplacian_diffusion(self, array_name, dt, num_steps) -> Self:
1087    def laplacian_diffusion(self, array_name, dt, num_steps) -> Self:
1088        """
1089        Apply a diffusion process to a scalar array defined on the points of a mesh.
1090
1091        Arguments:
1092            array_name : (str)
1093                name of the array to diffuse.
1094            dt : (float)
1095                time step.
1096            num_steps : (int)
1097                number of iterations.
1098        """
1099        try:
1100            import scipy.sparse
1101            import scipy.sparse.linalg
1102        except ImportError:
1103            vedo.logger.error("scipy not found. Cannot run laplacian_diffusion()")
1104            return self
1105
1106        def build_laplacian():
1107            rows = []
1108            cols = []
1109            data = []
1110            n_points = points.shape[0]
1111            avg_area = np.mean(areas) * 10000
1112            # print("avg_area", avg_area)
1113
1114            for triangle in cells:
1115                for i in range(3):
1116                    for j in range(i + 1, 3):
1117                        u = triangle[i]
1118                        v = triangle[j]
1119                        rows.append(u)
1120                        cols.append(v)
1121                        rows.append(v)
1122                        cols.append(u)
1123                        data.append(-1/avg_area)
1124                        data.append(-1/avg_area)
1125
1126            L = scipy.sparse.coo_matrix(
1127                (data, (rows, cols)), shape=(n_points, n_points)
1128            ).tocsc()
1129
1130            degree = -np.array(L.sum(axis=1)).flatten() # adjust the diagonal
1131            # print("degree", degree)
1132            L.setdiag(degree)
1133            return L
1134
1135        def _diffuse(u0, L, dt, num_steps):
1136            # mean_area = np.mean(areas) * 10000
1137            # print("mean_area", mean_area)
1138            mean_area = 1
1139            I = scipy.sparse.eye(L.shape[0], format="csc")
1140            A = I - (dt/mean_area) * L
1141            u = u0
1142            for _ in range(int(num_steps)):
1143                u = A.dot(u)
1144            return u
1145
1146        self.compute_cell_size()
1147        areas = self.celldata["Area"]
1148        points = self.coordinates
1149        cells = self.cells
1150        u0 = self.pointdata[array_name]
1151
1152        # Simulate diffusion
1153        L = build_laplacian()
1154        u = _diffuse(u0, L, dt, num_steps)
1155        self.pointdata[array_name] = u
1156        return self

Apply a diffusion process to a scalar array defined on the points of a mesh.

Arguments:
  • array_name : (str) name of the array to diffuse.
  • dt : (float) time step.
  • num_steps : (int) number of iterations.
def subdivide(self, n=1, method=0, mel=None) -> Self:
1159    def subdivide(self, n=1, method=0, mel=None) -> Self:
1160        """
1161        Increase the number of vertices of a surface mesh.
1162
1163        Arguments:
1164            n : (int)
1165                number of subdivisions.
1166            method : (int)
1167                Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
1168            mel : (float)
1169                Maximum Edge Length (applicable to Adaptive method only).
1170        """
1171        triangles = vtki.new("TriangleFilter")
1172        triangles.SetInputData(self.dataset)
1173        triangles.Update()
1174        tri_mesh = triangles.GetOutput()
1175        if method == 0:
1176            sdf = vtki.new("LoopSubdivisionFilter")
1177        elif method == 1:
1178            sdf = vtki.new("LinearSubdivisionFilter")
1179        elif method == 2:
1180            sdf = vtki.new("AdaptiveSubdivisionFilter")
1181            if mel is None:
1182                mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n
1183            sdf.SetMaximumEdgeLength(mel)
1184        elif method == 3:
1185            sdf = vtki.new("ButterflySubdivisionFilter")
1186        elif method == 4:
1187            sdf = vtki.new("DensifyPolyData")
1188        else:
1189            vedo.logger.error(f"in subdivide() unknown method {method}")
1190            raise RuntimeError()
1191
1192        if method != 2:
1193            sdf.SetNumberOfSubdivisions(n)
1194
1195        sdf.SetInputData(tri_mesh)
1196        sdf.Update()
1197
1198        self._update(sdf.GetOutput())
1199
1200        self.pipeline = OperationNode(
1201            "subdivide",
1202            parents=[self],
1203            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1204        )
1205        return self

Increase the number of vertices of a surface mesh.

Arguments:
  • n : (int) number of subdivisions.
  • method : (int) Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
  • mel : (float) Maximum Edge Length (applicable to Adaptive method only).
def decimate( self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self:
1208    def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self:
1209        """
1210        Downsample the number of vertices in a mesh to `fraction`.
1211
1212        This filter preserves the `pointdata` of the input dataset. In previous versions
1213        of vedo, this decimation algorithm was referred to as quadric decimation.
1214
1215        Arguments:
1216            fraction : (float)
1217                the desired target of reduction.
1218            n : (int)
1219                the desired number of final points
1220                (`fraction` is recalculated based on it).
1221            preserve_volume : (bool)
1222                Decide whether to activate volume preservation which greatly
1223                reduces errors in triangle normal direction.
1224            regularization : (float)
1225                regularize the point finding algorithm so as to have better quality
1226                mesh elements at the cost of a slightly lower precision on the
1227                geometry potentially (mostly at sharp edges).
1228                Can be useful for decimating meshes that have been triangulated on noisy data.
1229
1230        Note:
1231            Setting `fraction=0.1` leaves 10% of the original number of vertices.
1232            Internally the VTK class
1233            [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html)
1234            is used for this operation.
1235
1236        See also: `decimate_binned()` and `decimate_pro()`.
1237        """
1238        poly = self.dataset
1239        if n:  # N = desired number of points
1240            npt = poly.GetNumberOfPoints()
1241            fraction = n / npt
1242            if fraction >= 1:
1243                return self
1244
1245        decimate = vtki.new("QuadricDecimation")
1246        decimate.SetVolumePreservation(preserve_volume)
1247        # decimate.AttributeErrorMetricOn()
1248        if regularization:
1249            decimate.SetRegularize(True)
1250            decimate.SetRegularization(regularization)
1251
1252        try:
1253            decimate.MapPointDataOn()
1254        except AttributeError:
1255            pass
1256
1257        decimate.SetTargetReduction(1 - fraction)
1258        decimate.SetInputData(poly)
1259        decimate.Update()
1260
1261        self._update(decimate.GetOutput())
1262        self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction()
1263
1264        self.pipeline = OperationNode(
1265            "decimate",
1266            parents=[self],
1267            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1268        )
1269        return self

Downsample the number of vertices in a mesh to fraction.

This filter preserves the pointdata of the input dataset. In previous versions of vedo, this decimation algorithm was referred to as quadric decimation.

Arguments:
  • fraction : (float) the desired target of reduction.
  • n : (int) the desired number of final points (fraction is recalculated based on it).
  • preserve_volume : (bool) Decide whether to activate volume preservation which greatly reduces errors in triangle normal direction.
  • regularization : (float) regularize the point finding algorithm so as to have better quality mesh elements at the cost of a slightly lower precision on the geometry potentially (mostly at sharp edges). Can be useful for decimating meshes that have been triangulated on noisy data.
Note:

Setting fraction=0.1 leaves 10% of the original number of vertices. Internally the VTK class vtkQuadricDecimation is used for this operation.

See also: decimate_binned() and decimate_pro().

def decimate_pro( self, fraction=0.5, n=None, preserve_topology=True, preserve_boundaries=True, splitting=False, splitting_angle=75, feature_angle=0, inflection_point_ratio=10, vertex_degree=0) -> Self:
1271    def decimate_pro(
1272            self,
1273            fraction=0.5,
1274            n=None,
1275            preserve_topology=True,
1276            preserve_boundaries=True,
1277            splitting=False,
1278            splitting_angle=75,
1279            feature_angle=0,
1280            inflection_point_ratio=10,
1281            vertex_degree=0,
1282        ) -> Self:
1283        """
1284        Downsample the number of vertices in a mesh to `fraction`.
1285
1286        This filter preserves the `pointdata` of the input dataset.
1287
1288        Arguments:
1289            fraction : (float)
1290                The desired target of reduction.
1291                Setting `fraction=0.1` leaves 10% of the original number of vertices.
1292            n : (int)
1293                the desired number of final points (`fraction` is recalculated based on it).
1294            preserve_topology : (bool)
1295                If on, mesh splitting and hole elimination will not occur.
1296                This may limit the maximum reduction that may be achieved.
1297            preserve_boundaries : (bool)
1298                Turn on/off the deletion of vertices on the boundary of a mesh.
1299                Control whether mesh boundaries are preserved during decimation.
1300            feature_angle : (float)
1301                Specify the angle that defines a feature.
1302                This angle is used to define what an edge is
1303                (i.e., if the surface normal between two adjacent triangles
1304                is >= FeatureAngle, an edge exists).
1305            splitting : (bool)
1306                Turn on/off the splitting of the mesh at corners,
1307                along edges, at non-manifold points, or anywhere else a split is required.
1308                Turning splitting off will better preserve the original topology of the mesh,
1309                but you may not obtain the requested reduction.
1310            splitting_angle : (float)
1311                Specify the angle that defines a sharp edge.
1312                This angle is used to control the splitting of the mesh.
1313                A split line exists when the surface normals between two edge connected triangles
1314                are >= `splitting_angle`.
1315            inflection_point_ratio : (float)
1316                An inflection point occurs when the ratio of reduction error between two iterations
1317                is greater than or equal to the `inflection_point_ratio` value.
1318            vertex_degree : (int)
1319                If the number of triangles connected to a vertex exceeds it then the vertex will be split.
1320
1321        Note:
1322            Setting `fraction=0.1` leaves 10% of the original number of vertices
1323
1324        See also:
1325            `decimate()` and `decimate_binned()`.
1326        """
1327        poly = self.dataset
1328        if n:  # N = desired number of points
1329            npt = poly.GetNumberOfPoints()
1330            fraction = n / npt
1331            if fraction >= 1:
1332                return self
1333
1334        decimate = vtki.new("DecimatePro")
1335        decimate.SetPreserveTopology(preserve_topology)
1336        decimate.SetBoundaryVertexDeletion(preserve_boundaries)
1337        if feature_angle:
1338            decimate.SetFeatureAngle(feature_angle)
1339        decimate.SetSplitting(splitting)
1340        decimate.SetSplitAngle(splitting_angle)
1341        decimate.SetInflectionPointRatio(inflection_point_ratio)
1342        if vertex_degree:
1343            decimate.SetDegree(vertex_degree)
1344
1345        decimate.SetTargetReduction(1 - fraction)
1346        decimate.SetInputData(poly)
1347        decimate.Update()
1348        self._update(decimate.GetOutput())
1349
1350        self.pipeline = OperationNode(
1351            "decimate_pro",
1352            parents=[self],
1353            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1354        )
1355        return self

Downsample the number of vertices in a mesh to fraction.

This filter preserves the pointdata of the input dataset.

Arguments:
  • fraction : (float) The desired target of reduction. Setting fraction=0.1 leaves 10% of the original number of vertices.
  • n : (int) the desired number of final points (fraction is recalculated based on it).
  • preserve_topology : (bool) If on, mesh splitting and hole elimination will not occur. This may limit the maximum reduction that may be achieved.
  • preserve_boundaries : (bool) Turn on/off the deletion of vertices on the boundary of a mesh. Control whether mesh boundaries are preserved during decimation.
  • feature_angle : (float) Specify the angle that defines a feature. This angle is used to define what an edge is (i.e., if the surface normal between two adjacent triangles is >= FeatureAngle, an edge exists).
  • splitting : (bool) Turn on/off the splitting of the mesh at corners, along edges, at non-manifold points, or anywhere else a split is required. Turning splitting off will better preserve the original topology of the mesh, but you may not obtain the requested reduction.
  • splitting_angle : (float) Specify the angle that defines a sharp edge. This angle is used to control the splitting of the mesh. A split line exists when the surface normals between two edge connected triangles are >= splitting_angle.
  • inflection_point_ratio : (float) An inflection point occurs when the ratio of reduction error between two iterations is greater than or equal to the inflection_point_ratio value.
  • vertex_degree : (int) If the number of triangles connected to a vertex exceeds it then the vertex will be split.
Note:

Setting fraction=0.1 leaves 10% of the original number of vertices

See also:

decimate() and decimate_binned().

def decimate_binned(self, divisions=(), use_clustering=False) -> Self:
1357    def decimate_binned(self, divisions=(), use_clustering=False) -> Self:
1358        """
1359        Downsample the number of vertices in a mesh.
1360
1361        This filter preserves the `celldata` of the input dataset,
1362        if `use_clustering=True` also the `pointdata` will be preserved in the result.
1363
1364        Arguments:
1365            divisions : (list)
1366                number of divisions along x, y and z axes.
1367            auto_adjust : (bool)
1368                if True, the number of divisions is automatically adjusted to
1369                create more uniform cells.
1370            use_clustering : (bool)
1371                use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html)
1372                instead of
1373                [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html).
1374
1375        See also: `decimate()` and `decimate_pro()`.
1376        """
1377        if use_clustering:
1378            decimate = vtki.new("QuadricClustering")
1379            decimate.CopyCellDataOn()
1380        else:
1381            decimate = vtki.new("BinnedDecimation")
1382            decimate.ProducePointDataOn()
1383            decimate.ProduceCellDataOn()
1384
1385        decimate.SetInputData(self.dataset)
1386
1387        if len(divisions) == 0:
1388            decimate.SetAutoAdjustNumberOfDivisions(1)
1389        else:
1390            decimate.SetAutoAdjustNumberOfDivisions(0)
1391            decimate.SetNumberOfDivisions(divisions)
1392        decimate.Update()
1393
1394        self._update(decimate.GetOutput())
1395        self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions()
1396        self.pipeline = OperationNode(
1397            "decimate_binned",
1398            parents=[self],
1399            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1400        )
1401        return self

Downsample the number of vertices in a mesh.

This filter preserves the celldata of the input dataset, if use_clustering=True also the pointdata will be preserved in the result.

Arguments:
  • divisions : (list) number of divisions along x, y and z axes.
  • auto_adjust : (bool) if True, the number of divisions is automatically adjusted to create more uniform cells.
  • use_clustering : (bool) use vtkQuadricClustering instead of vtkBinnedDecimation.

See also: decimate() and decimate_pro().

def generate_random_points(self, n: int, min_radius=0.0) -> vedo.pointcloud.Points:
1403    def generate_random_points(self, n: int, min_radius=0.0) -> "Points":
1404        """
1405        Generate `n` uniformly distributed random points
1406        inside the polygonal mesh.
1407
1408        A new point data array is added to the output points
1409        called "OriginalCellID" which contains the index of
1410        the cell ID in which the point was generated.
1411
1412        Arguments:
1413            n : (int)
1414                number of points to generate.
1415            min_radius: (float)
1416                impose a minimum distance between points.
1417                If `min_radius` is set to 0, the points are
1418                generated uniformly at random inside the mesh.
1419                If `min_radius` is set to a positive value,
1420                the points are generated uniformly at random
1421                inside the mesh, but points closer than `min_radius`
1422                to any other point are discarded.
1423
1424        Returns a `vedo.Points` object.
1425
1426        Note:
1427            Consider using `points.probe(msh)` or
1428            `points.interpolate_data_from(msh)`
1429            to interpolate existing mesh data onto the new points.
1430
1431        Example:
1432        ```python
1433        from vedo import *
1434        msh = Mesh(dataurl + "panther.stl").lw(2)
1435        pts = msh.generate_random_points(20000, min_radius=0.5)
1436        print("Original cell ids:", pts.pointdata["OriginalCellID"])
1437        show(pts, msh, axes=1).close()
1438        ```
1439        """
1440        cmesh = self.clone().clean().triangulate().compute_cell_size()
1441        triangles = cmesh.cells
1442        vertices = cmesh.vertices
1443        cumul = np.cumsum(cmesh.celldata["Area"])
1444
1445        out_pts = []
1446        orig_cell = []
1447        for _ in range(n):
1448            # choose a triangle based on area
1449            random_area = np.random.random() * cumul[-1]
1450            it = np.searchsorted(cumul, random_area)
1451            A, B, C = vertices[triangles[it]]
1452            # calculate the random point in the triangle
1453            r1, r2 = np.random.random(2)
1454            if r1 + r2 > 1:
1455                r1 = 1 - r1
1456                r2 = 1 - r2
1457            out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C)
1458            orig_cell.append(it)
1459        nporig_cell = np.array(orig_cell, dtype=np.uint32)
1460
1461        vpts = Points(out_pts)
1462        vpts.pointdata["OriginalCellID"] = nporig_cell
1463
1464        if min_radius > 0:
1465            vpts.subsample(min_radius, absolute=True)
1466
1467        vpts.point_size(5).color("k1")
1468        vpts.name = "RandomPoints"
1469        vpts.pipeline = OperationNode(
1470            "generate_random_points", c="#edabab", parents=[self])
1471        return vpts

Generate n uniformly distributed random points inside the polygonal mesh.

A new point data array is added to the output points called "OriginalCellID" which contains the index of the cell ID in which the point was generated.

Arguments:
  • n : (int) number of points to generate.
  • min_radius: (float) impose a minimum distance between points. If min_radius is set to 0, the points are generated uniformly at random inside the mesh. If min_radius is set to a positive value, the points are generated uniformly at random inside the mesh, but points closer than min_radius to any other point are discarded.

Returns a vedo.Points object.

Note:

Consider using points.probe(msh) or points.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()
def delete_cells(self, ids: List[int]) -> Self:
1473    def delete_cells(self, ids: List[int]) -> Self:
1474        """
1475        Remove cells from the mesh object by their ID.
1476        Points (vertices) are not removed (you may use `clean()` to remove those).
1477        """
1478        self.dataset.BuildLinks()
1479        for cid in ids:
1480            self.dataset.DeleteCell(cid)
1481        self.dataset.RemoveDeletedCells()
1482        self.dataset.Modified()
1483        self.mapper.Modified()
1484        self.pipeline = OperationNode(
1485            "delete_cells",
1486            parents=[self],
1487            comment=f"#cells {self.dataset.GetNumberOfCells()}",
1488        )
1489        return self

Remove cells from the mesh object by their ID. Points (vertices) are not removed (you may use clean() to remove those).

def delete_cells_by_point_index(self, indices: List[int]) -> Self:
1491    def delete_cells_by_point_index(self, indices: List[int]) -> Self:
1492        """
1493        Delete a list of vertices identified by any of their vertex index.
1494
1495        See also `delete_cells()`.
1496
1497        Examples:
1498            - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py)
1499
1500                ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png)
1501        """
1502        cell_ids = vtki.vtkIdList()
1503        self.dataset.BuildLinks()
1504        n = 0
1505        for i in np.unique(indices):
1506            self.dataset.GetPointCells(i, cell_ids)
1507            for j in range(cell_ids.GetNumberOfIds()):
1508                self.dataset.DeleteCell(cell_ids.GetId(j))  # flag cell
1509                n += 1
1510
1511        self.dataset.RemoveDeletedCells()
1512        self.dataset.Modified()
1513        self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self])
1514        return self

Delete a list of vertices identified by any of their vertex index.

See also delete_cells().

Examples:
def collapse_edges(self, distance: float, iterations=1) -> Self:
1516    def collapse_edges(self, distance: float, iterations=1) -> Self:
1517        """
1518        Collapse mesh edges so that are all above `distance`.
1519
1520        Example:
1521            ```python
1522            from vedo import *
1523            np.random.seed(2)
1524            grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
1525            grid1.celldata['scalar'] = grid1.cell_centers().coordinates[:,1]
1526            grid2 = grid1.clone().collapse_edges(0.1)
1527            show(grid1, grid2, N=2, axes=1)
1528            ```
1529        """
1530        for _ in range(iterations):
1531            medges = self.edges
1532            pts = self.vertices
1533            newpts = np.array(pts)
1534            moved = []
1535            for e in medges:
1536                if len(e) == 2:
1537                    id0, id1 = e
1538                    p0, p1 = pts[id0], pts[id1]
1539                    if (np.linalg.norm(p1-p0) < distance
1540                        and id0 not in moved
1541                        and id1 not in moved
1542                    ):
1543                        p = (p0 + p1) / 2
1544                        newpts[id0] = p
1545                        newpts[id1] = p
1546                        moved += [id0, id1]
1547            self.vertices = newpts
1548            cpd = vtki.new("CleanPolyData")
1549            cpd.ConvertLinesToPointsOff()
1550            cpd.ConvertPolysToLinesOff()
1551            cpd.ConvertStripsToPolysOff()
1552            cpd.SetInputData(self.dataset)
1553            cpd.Update()
1554            self._update(cpd.GetOutput())
1555
1556        self.pipeline = OperationNode(
1557            "collapse_edges",
1558            parents=[self],
1559            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1560        )
1561        return self

Collapse mesh edges so that are all above distance.

Example:
from vedo import *
np.random.seed(2)
grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
grid1.celldata['scalar'] = grid1.cell_centers().coordinates[:,1]
grid2 = grid1.clone().collapse_edges(0.1)
show(grid1, grid2, N=2, axes=1)
def adjacency_list(self) -> List[set]:
1563    def adjacency_list(self) -> List[set]:
1564        """
1565        Computes the adjacency list for mesh edge-graph.
1566
1567        Returns:
1568            a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex
1569        """
1570        inc = [set()] * self.npoints
1571        for cell in self.cells:
1572            nc = len(cell)
1573            if nc > 1:
1574                for i in range(nc-1):
1575                    ci = cell[i]
1576                    inc[ci] = inc[ci].union({cell[i-1], cell[i+1]})
1577        return inc

Computes the adjacency list for mesh edge-graph.

Returns:

a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex

def graph_ball(self, index, n: int) -> set:
1579    def graph_ball(self, index, n: int) -> set:
1580        """
1581        Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`.
1582
1583        Arguments:
1584            index : (int)
1585                index of the vertex
1586            n : (int)
1587                radius in the graph metric
1588
1589        Returns:
1590            the set of indices of the vertices which are at most `n` edges from vertex `index`.
1591        """
1592        if n == 0:
1593            return {index}
1594        else:
1595            al = self.adjacency_list()
1596            ball = {index}
1597            i = 0
1598            while i < n and len(ball) < self.npoints:
1599                for v in ball:
1600                    ball = ball.union(al[v])
1601                i += 1
1602            return ball

Computes the ball of radius n in the mesh' edge-graph metric centred in vertex index.

Arguments:
  • index : (int) index of the vertex
  • n : (int) radius in the graph metric
Returns:

the set of indices of the vertices which are at most n edges from vertex index.

def smooth( self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self:
1604    def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self:
1605        """
1606        Adjust mesh point positions using the so-called "Windowed Sinc" method.
1607
1608        Arguments:
1609            niter : (int)
1610                number of iterations.
1611            pass_band : (float)
1612                set the pass_band value for the windowed sinc filter.
1613            edge_angle : (float)
1614                edge angle to control smoothing along edges (either interior or boundary).
1615            feature_angle : (float)
1616                specifies the feature angle for sharp edge identification.
1617            boundary : (bool)
1618                specify if boundary should also be smoothed or kept unmodified
1619
1620        Examples:
1621            - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py)
1622
1623            ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png)
1624        """
1625        cl = vtki.new("CleanPolyData")
1626        cl.SetInputData(self.dataset)
1627        cl.Update()
1628        smf = vtki.new("WindowedSincPolyDataFilter")
1629        smf.SetInputData(cl.GetOutput())
1630        smf.SetNumberOfIterations(niter)
1631        smf.SetEdgeAngle(edge_angle)
1632        smf.SetFeatureAngle(feature_angle)
1633        smf.SetPassBand(pass_band)
1634        smf.NormalizeCoordinatesOn()
1635        smf.NonManifoldSmoothingOn()
1636        smf.FeatureEdgeSmoothingOn()
1637        smf.SetBoundarySmoothing(boundary)
1638        smf.Update()
1639
1640        self._update(smf.GetOutput())
1641
1642        self.pipeline = OperationNode(
1643            "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1644        )
1645        return self

Adjust mesh point positions using the so-called "Windowed Sinc" method.

Arguments:
  • niter : (int) number of iterations.
  • pass_band : (float) set the pass_band value for the windowed sinc filter.
  • edge_angle : (float) edge angle to control smoothing along edges (either interior or boundary).
  • feature_angle : (float) specifies the feature angle for sharp edge identification.
  • boundary : (bool) specify if boundary should also be smoothed or kept unmodified
Examples:

def fill_holes(self, size=None) -> Self:
1647    def fill_holes(self, size=None) -> Self:
1648        """
1649        Identifies and fills holes in the input mesh.
1650        Holes are identified by locating boundary edges, linking them together
1651        into loops, and then triangulating the resulting loops.
1652
1653        Arguments:
1654            size : (float)
1655                Approximate limit to the size of the hole that can be filled.
1656
1657        Examples:
1658            - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py)
1659        """
1660        fh = vtki.new("FillHolesFilter")
1661        if not size:
1662            mb = self.diagonal_size()
1663            size = mb / 10
1664        fh.SetHoleSize(size)
1665        fh.SetInputData(self.dataset)
1666        fh.Update()
1667
1668        self._update(fh.GetOutput())
1669
1670        self.pipeline = OperationNode(
1671            "fill_holes",
1672            parents=[self],
1673            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1674        )
1675        return self

Identifies and fills holes in the input mesh. Holes are identified by locating boundary edges, linking them together into loops, and then triangulating the resulting loops.

Arguments:
  • size : (float) Approximate limit to the size of the hole that can be filled.
Examples:
def contains(self, point: tuple, tol=1e-05) -> bool:
1677    def contains(self, point: tuple, tol=1e-05) -> bool:
1678        """
1679        Return True if point is inside a polydata closed surface.
1680
1681        Note:
1682            if you have many points to check use `inside_points()` instead.
1683
1684        Example:
1685            ```python
1686            from vedo import *
1687            s = Sphere().c('green5').alpha(0.5)
1688            pt  = [0.1, 0.2, 0.3]
1689            print("Sphere contains", pt, s.contains(pt))
1690            show(s, Point(pt), axes=1).close()
1691            ```
1692        """
1693        points = vtki.vtkPoints()
1694        points.InsertNextPoint(point)
1695        poly = vtki.vtkPolyData()
1696        poly.SetPoints(points)
1697        sep = vtki.new("SelectEnclosedPoints")
1698        sep.SetTolerance(tol)
1699        sep.CheckSurfaceOff()
1700        sep.SetInputData(poly)
1701        sep.SetSurfaceData(self.dataset)
1702        sep.Update()
1703        return bool(sep.IsInside(0))

Return True if point is inside a polydata closed surface.

Note:

if you have many points to check use inside_points() instead.

Example:
from vedo import *
s = Sphere().c('green5').alpha(0.5)
pt  = [0.1, 0.2, 0.3]
print("Sphere contains", pt, s.contains(pt))
show(s, Point(pt), axes=1).close()
def inside_points( self, pts: Union[vedo.pointcloud.Points, list], invert=False, tol=1e-05, return_ids=False) -> Union[vedo.pointcloud.Points, numpy.ndarray]:
1705    def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]:
1706        """
1707        Return the point cloud that is inside mesh surface as a new Points object.
1708
1709        If return_ids is True a list of IDs is returned and in addition input points
1710        are marked by a pointdata array named "IsInside".
1711
1712        Example:
1713            `print(pts.pointdata["IsInside"])`
1714
1715        Examples:
1716            - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
1717
1718            ![](https://vedo.embl.es/images/basic/pca.png)
1719        """
1720        if isinstance(pts, Points):
1721            poly = pts.dataset
1722            ptsa = pts.coordinates
1723        else:
1724            ptsa = np.asarray(pts)
1725            vpoints = vtki.vtkPoints()
1726            vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32))
1727            poly = vtki.vtkPolyData()
1728            poly.SetPoints(vpoints)
1729
1730        sep = vtki.new("SelectEnclosedPoints")
1731        # sep = vtki.new("ExtractEnclosedPoints()
1732        sep.SetTolerance(tol)
1733        sep.SetInputData(poly)
1734        sep.SetSurfaceData(self.dataset)
1735        sep.SetInsideOut(invert)
1736        sep.Update()
1737
1738        varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints")
1739        mask = vtk2numpy(varr).astype(bool)
1740        ids = np.array(range(len(ptsa)), dtype=int)[mask]
1741
1742        if isinstance(pts, Points):
1743            varr.SetName("IsInside")
1744            pts.dataset.GetPointData().AddArray(varr)
1745
1746        if return_ids:
1747            return ids
1748
1749        pcl = Points(ptsa[ids])
1750        pcl.name = "InsidePoints"
1751
1752        pcl.pipeline = OperationNode(
1753            "inside_points",
1754            parents=[self, ptsa],
1755            comment=f"#pts {pcl.dataset.GetNumberOfPoints()}",
1756        )
1757        return pcl

Return the point cloud that is inside mesh surface as a new Points object.

If return_ids is True a list of IDs is returned and in addition input points are marked by a pointdata array named "IsInside".

Example:

print(pts.pointdata["IsInside"])

Examples:

def boundaries( self, boundary_edges=True, manifold_edges=False, non_manifold_edges=False, feature_angle=None, return_point_ids=False, return_cell_ids=False, cell_edge=False) -> Union[Self, numpy.ndarray]:
1759    def boundaries(
1760        self,
1761        boundary_edges=True,
1762        manifold_edges=False,
1763        non_manifold_edges=False,
1764        feature_angle=None,
1765        return_point_ids=False,
1766        return_cell_ids=False,
1767        cell_edge=False,
1768    ) -> Union[Self, np.ndarray]:
1769        """
1770        Return the boundary lines of an input mesh.
1771        Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method.
1772
1773        Arguments:
1774            boundary_edges : (bool)
1775                Turn on/off the extraction of boundary edges.
1776            manifold_edges : (bool)
1777                Turn on/off the extraction of manifold edges.
1778            non_manifold_edges : (bool)
1779                Turn on/off the extraction of non-manifold edges.
1780            feature_angle : (bool)
1781                Specify the min angle btw 2 faces for extracting edges.
1782            return_point_ids : (bool)
1783                return a numpy array of point indices
1784            return_cell_ids : (bool)
1785                return a numpy array of cell indices
1786            cell_edge : (bool)
1787                set to `True` if a cell need to share an edge with
1788                the boundary line, or `False` if a single vertex is enough
1789
1790        Examples:
1791            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)
1792
1793            ![](https://vedo.embl.es/images/basic/boundaries.png)
1794        """
1795        fe = vtki.new("FeatureEdges")
1796        fe.SetBoundaryEdges(boundary_edges)
1797        fe.SetNonManifoldEdges(non_manifold_edges)
1798        fe.SetManifoldEdges(manifold_edges)
1799        try:
1800            fe.SetPassLines(True) # vtk9.2
1801        except AttributeError:
1802            pass
1803        fe.ColoringOff()
1804        fe.SetFeatureEdges(False)
1805        if feature_angle is not None:
1806            fe.SetFeatureEdges(True)
1807            fe.SetFeatureAngle(feature_angle)
1808
1809        if return_point_ids or return_cell_ids:
1810            idf = vtki.new("IdFilter")
1811            idf.SetInputData(self.dataset)
1812            idf.SetPointIdsArrayName("BoundaryIds")
1813            idf.SetPointIds(True)
1814            idf.Update()
1815
1816            fe.SetInputData(idf.GetOutput())
1817            fe.Update()
1818
1819            vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds")
1820            npid = vtk2numpy(vid).astype(int)
1821
1822            if return_point_ids:
1823                return npid
1824
1825            if return_cell_ids:
1826                n = 1 if cell_edge else 0
1827                inface = []
1828                for i, face in enumerate(self.cells):
1829                    # isin = np.any([vtx in npid for vtx in face])
1830                    isin = 0
1831                    for vtx in face:
1832                        isin += int(vtx in npid)
1833                        if isin > n:
1834                            break
1835                    if isin > n:
1836                        inface.append(i)
1837                return np.array(inface).astype(int)
1838
1839            return self
1840
1841        else:
1842
1843            fe.SetInputData(self.dataset)
1844            fe.Update()
1845            msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off")
1846            msh.name = "MeshBoundaries"
1847
1848            msh.pipeline = OperationNode(
1849                "boundaries",
1850                parents=[self],
1851                shape="octagon",
1852                comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
1853            )
1854            return msh

Return the boundary lines of an input mesh. Check also vedo.core.CommonAlgorithms.mark_boundaries() method.

Arguments:
  • boundary_edges : (bool) Turn on/off the extraction of boundary edges.
  • manifold_edges : (bool) Turn on/off the extraction of manifold edges.
  • non_manifold_edges : (bool) Turn on/off the extraction of non-manifold edges.
  • feature_angle : (bool) Specify the min angle btw 2 faces for extracting edges.
  • return_point_ids : (bool) return a numpy array of point indices
  • return_cell_ids : (bool) return a numpy array of cell indices
  • cell_edge : (bool) set to True if a cell need to share an edge with the boundary line, or False if a single vertex is enough
Examples:

def imprint(self, loopline, tol=0.01) -> Self:
1856    def imprint(self, loopline, tol=0.01) -> Self:
1857        """
1858        Imprint the contact surface of one object onto another surface.
1859
1860        Arguments:
1861            loopline : (vedo.Line)
1862                a Line object to be imprinted onto the mesh.
1863            tol : (float)
1864                projection tolerance which controls how close the imprint
1865                surface must be to the target.
1866
1867        Example:
1868            ```python
1869            from vedo import *
1870            grid = Grid()#.triangulate()
1871            circle = Circle(r=0.3, res=24).pos(0.11,0.12)
1872            line = Line(circle, closed=True, lw=4, c='r4')
1873            grid.imprint(line)
1874            show(grid, line, axes=1).close()
1875            ```
1876            ![](https://vedo.embl.es/images/feats/imprint.png)
1877        """
1878        loop = vtki.new("ContourLoopExtraction")
1879        loop.SetInputData(loopline.dataset)
1880        loop.Update()
1881
1882        clean_loop = vtki.new("CleanPolyData")
1883        clean_loop.SetInputData(loop.GetOutput())
1884        clean_loop.Update()
1885
1886        imp = vtki.new("ImprintFilter")
1887        imp.SetTargetData(self.dataset)
1888        imp.SetImprintData(clean_loop.GetOutput())
1889        imp.SetTolerance(tol)
1890        imp.BoundaryEdgeInsertionOn()
1891        imp.TriangulateOutputOn()
1892        imp.Update()
1893
1894        self._update(imp.GetOutput())
1895
1896        self.pipeline = OperationNode(
1897            "imprint",
1898            parents=[self],
1899            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1900        )
1901        return self

Imprint the contact surface of one object onto another surface.

Arguments:
  • loopline : (vedo.Line) a Line object to be imprinted onto the mesh.
  • tol : (float) projection tolerance which controls how close the imprint surface must be to the target.
Example:
from vedo import *
grid = Grid()#.triangulate()
circle = Circle(r=0.3, res=24).pos(0.11,0.12)
line = Line(circle, closed=True, lw=4, c='r4')
grid.imprint(line)
show(grid, line, axes=1).close()

def connected_vertices(self, index: int) -> List[int]:
1903    def connected_vertices(self, index: int) -> List[int]:
1904        """Find all vertices connected to an input vertex specified by its index.
1905
1906        Examples:
1907            - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py)
1908
1909            ![](https://vedo.embl.es/images/basic/connVtx.png)
1910        """
1911        poly = self.dataset
1912
1913        cell_idlist = vtki.vtkIdList()
1914        poly.GetPointCells(index, cell_idlist)
1915
1916        idxs = []
1917        for i in range(cell_idlist.GetNumberOfIds()):
1918            point_idlist = vtki.vtkIdList()
1919            poly.GetCellPoints(cell_idlist.GetId(i), point_idlist)
1920            for j in range(point_idlist.GetNumberOfIds()):
1921                idj = point_idlist.GetId(j)
1922                if idj == index:
1923                    continue
1924                if idj in idxs:
1925                    continue
1926                idxs.append(idj)
1927
1928        return idxs

Find all vertices connected to an input vertex specified by its index.

Examples:

def extract_cells(self, ids: List[int]) -> Self:
1930    def extract_cells(self, ids: List[int]) -> Self:
1931        """
1932        Extract a subset of cells from a mesh and return it as a new mesh.
1933        """
1934        selectCells = vtki.new("SelectionNode")
1935        selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL)
1936        selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES)
1937        idarr = vtki.vtkIdTypeArray()
1938        idarr.SetNumberOfComponents(1)
1939        idarr.SetNumberOfValues(len(ids))
1940        for i, v in enumerate(ids):
1941            idarr.SetValue(i, v)
1942        selectCells.SetSelectionList(idarr)
1943
1944        selection = vtki.new("Selection")
1945        selection.AddNode(selectCells)
1946
1947        extractSelection = vtki.new("ExtractSelection")
1948        extractSelection.SetInputData(0, self.dataset)
1949        extractSelection.SetInputData(1, selection)
1950        extractSelection.Update()
1951
1952        gf = vtki.new("GeometryFilter")
1953        gf.SetInputData(extractSelection.GetOutput())
1954        gf.Update()
1955        msh = Mesh(gf.GetOutput())
1956        msh.copy_properties_from(self)
1957        return msh

Extract a subset of cells from a mesh and return it as a new mesh.

def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]:
1959    def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]:
1960        """Find all cellls connected to an input vertex specified by its index."""
1961
1962        # Find all cells connected to point index
1963        dpoly = self.dataset
1964        idlist = vtki.vtkIdList()
1965        dpoly.GetPointCells(index, idlist)
1966
1967        ids = vtki.vtkIdTypeArray()
1968        ids.SetNumberOfComponents(1)
1969        rids = []
1970        for k in range(idlist.GetNumberOfIds()):
1971            cid = idlist.GetId(k)
1972            ids.InsertNextValue(cid)
1973            rids.append(int(cid))
1974        if return_ids:
1975            return rids
1976
1977        selection_node = vtki.new("SelectionNode")
1978        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
1979        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
1980        selection_node.SetSelectionList(ids)
1981        selection = vtki.new("Selection")
1982        selection.AddNode(selection_node)
1983        extractSelection = vtki.new("ExtractSelection")
1984        extractSelection.SetInputData(0, dpoly)
1985        extractSelection.SetInputData(1, selection)
1986        extractSelection.Update()
1987        gf = vtki.new("GeometryFilter")
1988        gf.SetInputData(extractSelection.GetOutput())
1989        gf.Update()
1990        return Mesh(gf.GetOutput()).lw(1)

Find all cellls connected to an input vertex specified by its index.

def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self:
1992    def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self:
1993        """
1994        Return a new line `Mesh` which corresponds to the outer `silhouette`
1995        of the input as seen along a specified `direction`, this can also be
1996        a `vtkCamera` object.
1997
1998        Arguments:
1999            direction : (list)
2000                viewpoint direction vector.
2001                If `None` this is guessed by looking at the minimum
2002                of the sides of the bounding box.
2003            border_edges : (bool)
2004                enable or disable generation of border edges
2005            feature_angle : (float)
2006                minimal angle for sharp edges detection.
2007                If set to `False` the functionality is disabled.
2008
2009        Examples:
2010            - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py)
2011
2012            ![](https://vedo.embl.es/images/basic/silhouette1.png)
2013        """
2014        sil = vtki.new("PolyDataSilhouette")
2015        sil.SetInputData(self.dataset)
2016        sil.SetBorderEdges(border_edges)
2017        if feature_angle is False:
2018            sil.SetEnableFeatureAngle(0)
2019        else:
2020            sil.SetEnableFeatureAngle(1)
2021            sil.SetFeatureAngle(feature_angle)
2022
2023        if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera:
2024            sil.SetCamera(vedo.plotter_instance.camera)
2025            m = Mesh()
2026            m.mapper.SetInputConnection(sil.GetOutputPort())
2027
2028        elif isinstance(direction, vtki.vtkCamera):
2029            sil.SetCamera(direction)
2030            m = Mesh()
2031            m.mapper.SetInputConnection(sil.GetOutputPort())
2032
2033        elif direction == "2d":
2034            sil.SetVector(3.4, 4.5, 5.6)  # random
2035            sil.SetDirectionToSpecifiedVector()
2036            sil.Update()
2037            m = Mesh(sil.GetOutput())
2038
2039        elif is_sequence(direction):
2040            sil.SetVector(direction)
2041            sil.SetDirectionToSpecifiedVector()
2042            sil.Update()
2043            m = Mesh(sil.GetOutput())
2044        else:
2045            vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}")
2046            vedo.logger.error("first render the scene with show() or specify camera/direction")
2047            return self
2048
2049        m.lw(2).c((0, 0, 0)).lighting("off")
2050        m.mapper.SetResolveCoincidentTopologyToPolygonOffset()
2051        m.pipeline = OperationNode("silhouette", parents=[self])
2052        m.name = "Silhouette"
2053        return m

Return a new line Mesh which corresponds to the outer silhouette of the input as seen along a specified direction, this can also be a vtkCamera object.

Arguments:
  • direction : (list) viewpoint direction vector. If None this is guessed by looking at the minimum of the sides of the bounding box.
  • border_edges : (bool) enable or disable generation of border edges
  • feature_angle : (float) minimal angle for sharp edges detection. If set to False the functionality is disabled.
Examples:

def isobands(self, n=10, vmin=None, vmax=None) -> Self:
2055    def isobands(self, n=10, vmin=None, vmax=None) -> Self:
2056        """
2057        Return a new `Mesh` representing the isobands of the active scalars.
2058        This is a new mesh where the scalar is now associated to cell faces and
2059        used to colorize the mesh.
2060
2061        Arguments:
2062            n : (int)
2063                number of isobands in the range
2064            vmin : (float)
2065                minimum of the range
2066            vmax : (float)
2067                maximum of the range
2068
2069        Examples:
2070            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
2071        """
2072        r0, r1 = self.dataset.GetScalarRange()
2073        if vmin is None:
2074            vmin = r0
2075        if vmax is None:
2076            vmax = r1
2077
2078        # --------------------------------
2079        bands = []
2080        dx = (vmax - vmin) / float(n)
2081        b = [vmin, vmin + dx / 2.0, vmin + dx]
2082        i = 0
2083        while i < n:
2084            bands.append(b)
2085            b = [b[0] + dx, b[1] + dx, b[2] + dx]
2086            i += 1
2087
2088        # annotate, use the midpoint of the band as the label
2089        lut = self.mapper.GetLookupTable()
2090        labels = []
2091        for b in bands:
2092            labels.append("{:4.2f}".format(b[1]))
2093        values = vtki.vtkVariantArray()
2094        for la in labels:
2095            values.InsertNextValue(vtki.vtkVariant(la))
2096        for i in range(values.GetNumberOfTuples()):
2097            lut.SetAnnotation(i, values.GetValue(i).ToString())
2098
2099        bcf = vtki.new("BandedPolyDataContourFilter")
2100        bcf.SetInputData(self.dataset)
2101        # Use either the minimum or maximum value for each band.
2102        for i, band in enumerate(bands):
2103            bcf.SetValue(i, band[2])
2104        # We will use an indexed lookup table.
2105        bcf.SetScalarModeToIndex()
2106        bcf.GenerateContourEdgesOff()
2107        bcf.Update()
2108        bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands")
2109
2110        m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True)
2111        m1.mapper.SetLookupTable(lut)
2112        m1.mapper.SetScalarRange(lut.GetRange())
2113        m1.pipeline = OperationNode("isobands", parents=[self])
2114        m1.name = "IsoBands"
2115        return m1

Return a new Mesh representing the isobands of the active scalars. This is a new mesh where the scalar is now associated to cell faces and used to colorize the mesh.

Arguments:
  • n : (int) number of isobands in the range
  • vmin : (float) minimum of the range
  • vmax : (float) maximum of the range
Examples:
def isolines(self, n=10, vmin=None, vmax=None) -> Self:
2117    def isolines(self, n=10, vmin=None, vmax=None) -> Self:
2118        """
2119        Return a new `Mesh` representing the isolines of the active scalars.
2120
2121        Arguments:
2122            n : (int, list)
2123                number of isolines in the range, a list of specific values can also be passed.
2124            vmin : (float)
2125                minimum of the range
2126            vmax : (float)
2127                maximum of the range
2128
2129        Examples:
2130            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
2131
2132            ![](https://vedo.embl.es/images/pyplot/isolines.png)
2133        """
2134        bcf = vtki.new("ContourFilter")
2135        bcf.SetInputData(self.dataset)
2136        r0, r1 = self.dataset.GetScalarRange()
2137        if vmin is None:
2138            vmin = r0
2139        if vmax is None:
2140            vmax = r1
2141        if is_sequence(n):
2142            i=0
2143            for j in range(len(n)):
2144                if vmin<=n[j]<=vmax:
2145                    bcf.SetValue(i, n[i])
2146                    i += 1
2147                else:
2148                    #print("value out of range")
2149                    continue
2150        else:
2151            bcf.GenerateValues(n, vmin, vmax)
2152        bcf.Update()
2153        sf = vtki.new("Stripper")
2154        sf.SetJoinContiguousSegments(True)
2155        sf.SetInputData(bcf.GetOutput())
2156        sf.Update()
2157        cl = vtki.new("CleanPolyData")
2158        cl.SetInputData(sf.GetOutput())
2159        cl.Update()
2160        msh = Mesh(cl.GetOutput(), c="k").lighting("off")
2161        msh.mapper.SetResolveCoincidentTopologyToPolygonOffset()
2162        msh.pipeline = OperationNode("isolines", parents=[self])
2163        msh.name = "IsoLines"
2164        return msh

Return a new Mesh representing the isolines of the active scalars.

Arguments:
  • n : (int, list) number of isolines in the range, a list of specific values can also be passed.
  • vmin : (float) minimum of the range
  • vmax : (float) maximum of the range
Examples:

def extrude( self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self:
2166    def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self:
2167        """
2168        Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices.
2169        The input dataset is swept around the z-axis to create new polygonal primitives.
2170        For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.
2171
2172        You can control whether the sweep of a 2D object (i.e., polygon or triangle strip)
2173        is capped with the generating geometry.
2174        Also, you can control the angle of rotation, and whether translation along the z-axis
2175        is performed along with the rotation. (Translation is useful for creating "springs").
2176        You also can adjust the radius of the generating geometry using the "dR" keyword.
2177
2178        The skirt is generated by locating certain topological features.
2179        Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips)
2180        generate surfaces. This is true also of lines or polylines. Vertices generate lines.
2181
2182        This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses;
2183        or translational/rotational symmetric objects like springs or corkscrews.
2184
2185        Arguments:
2186            zshift : (float)
2187                shift along z axis.
2188            direction : (list)
2189                extrusion direction in the xy plane.
2190                note that zshift is forced to be the 3rd component of direction,
2191                which is therefore ignored.
2192            rotation : (float)
2193                set the angle of rotation.
2194            dr : (float)
2195                set the radius variation in absolute units.
2196            cap : (bool)
2197                enable or disable capping.
2198            res : (int)
2199                set the resolution of the generating geometry.
2200
2201        Warning:
2202            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result
2203            in two separate surfaces if capping is on, or no surface if capping is off.
2204
2205        Examples:
2206            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2207
2208            ![](https://vedo.embl.es/images/basic/extrude.png)
2209        """
2210        rf = vtki.new("RotationalExtrusionFilter")
2211        # rf = vtki.new("LinearExtrusionFilter")
2212        rf.SetInputData(self.dataset)  # must not be transformed
2213        rf.SetResolution(res)
2214        rf.SetCapping(cap)
2215        rf.SetAngle(rotation)
2216        rf.SetTranslation(zshift)
2217        rf.SetDeltaRadius(dr)
2218        rf.Update()
2219
2220        # convert triangle strips to polygonal data
2221        tris = vtki.new("TriangleFilter")
2222        tris.SetInputData(rf.GetOutput())
2223        tris.Update()
2224
2225        m = Mesh(tris.GetOutput())
2226
2227        if len(direction) > 1:
2228            p = self.pos()
2229            LT = vedo.LinearTransform()
2230            LT.translate(-p)
2231            LT.concatenate([
2232                [1, 0, direction[0]],
2233                [0, 1, direction[1]],
2234                [0, 0, 1]
2235            ])
2236            LT.translate(p)
2237            m.apply_transform(LT)
2238
2239        m.copy_properties_from(self).flat().lighting("default")
2240        m.pipeline = OperationNode(
2241            "extrude", parents=[self],
2242            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2243        )
2244        m.name = "ExtrudedMesh"
2245        return m

Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices. The input dataset is swept around the z-axis to create new polygonal primitives. For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.

You can control whether the sweep of a 2D object (i.e., polygon or triangle strip) is capped with the generating geometry. Also, you can control the angle of rotation, and whether translation along the z-axis is performed along with the rotation. (Translation is useful for creating "springs"). You also can adjust the radius of the generating geometry using the "dR" keyword.

The skirt is generated by locating certain topological features. Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips) generate surfaces. This is true also of lines or polylines. Vertices generate lines.

This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses; or translational/rotational symmetric objects like springs or corkscrews.

Arguments:
  • zshift : (float) shift along z axis.
  • direction : (list) extrusion direction in the xy plane. note that zshift is forced to be the 3rd component of direction, which is therefore ignored.
  • rotation : (float) set the angle of rotation.
  • dr : (float) set the radius variation in absolute units.
  • cap : (bool) enable or disable capping.
  • res : (int) set the resolution of the generating geometry.
Warning:

Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate surfaces if capping is on, or no surface if capping is off.

Examples:

def extrude_and_trim_with( self, surface: Mesh, direction=(), strategy='all', cap=True, cap_strategy='max') -> Self:
2247    def extrude_and_trim_with(
2248            self,
2249            surface: "Mesh",
2250            direction=(),
2251            strategy="all",
2252            cap=True,
2253            cap_strategy="max",
2254    ) -> Self:
2255        """
2256        Extrude a Mesh and trim it with an input surface mesh.
2257
2258        Arguments:
2259            surface : (Mesh)
2260                the surface mesh to trim with.
2261            direction : (list)
2262                extrusion direction in the xy plane.
2263            strategy : (str)
2264                either "boundary_edges" or "all_edges".
2265            cap : (bool)
2266                enable or disable capping.
2267            cap_strategy : (str)
2268                either "intersection", "minimum_distance", "maximum_distance", "average_distance".
2269
2270        The input Mesh is swept along a specified direction forming a "skirt"
2271        from the boundary edges 2D primitives (i.e., edges used by only one polygon);
2272        and/or from vertices and lines.
2273        The extent of the sweeping is limited by a second input: defined where
2274        the sweep intersects a user-specified surface.
2275
2276        Capping of the extrusion can be enabled.
2277        In this case the input, generating primitive is copied inplace as well
2278        as to the end of the extrusion skirt.
2279        (See warnings below on what happens if the intersecting sweep does not
2280        intersect, or partially intersects the trim surface.)
2281
2282        Note that this method operates in two fundamentally different modes
2283        based on the extrusion strategy.
2284        If the strategy is "boundary_edges", then only the boundary edges of the input's
2285        2D primitives are extruded (verts and lines are extruded to generate lines and quads).
2286        However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives
2287        is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).
2288
2289        Warning:
2290            The extrusion direction is assumed to define an infinite line.
2291            The intersection with the trim surface is along a ray from the - to + direction,
2292            however only the first intersection is taken.
2293            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate
2294            surfaces if capping is on and "boundary_edges" enabled,
2295            or no surface if capping is off and "boundary_edges" is enabled.
2296            If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface,
2297            then no output for that primitive will be generated. In extreme cases, it is possible that no output
2298            whatsoever will be generated.
2299
2300        Example:
2301            ```python
2302            from vedo import *
2303            sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
2304            circle = Circle([0,0,0], r=2, res=100).color('b6')
2305            extruded_circle = circle.extrude_and_trim_with(
2306                sphere,
2307                direction=[0,-0.2,1],
2308                strategy="bound",
2309                cap=True,
2310                cap_strategy="intersection",
2311            )
2312            circle.lw(3).color("tomato").shift(dz=-0.1)
2313            show(circle, sphere, extruded_circle, axes=1).close()
2314            ```
2315        """
2316        trimmer = vtki.new("TrimmedExtrusionFilter")
2317        trimmer.SetInputData(self.dataset)
2318        trimmer.SetCapping(cap)
2319        trimmer.SetExtrusionDirection(direction)
2320        trimmer.SetTrimSurfaceData(surface.dataset)
2321        if "bound" in strategy:
2322            trimmer.SetExtrusionStrategyToBoundaryEdges()
2323        elif "all" in strategy:
2324            trimmer.SetExtrusionStrategyToAllEdges()
2325        else:
2326            vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}")
2327        # print (trimmer.GetExtrusionStrategy())
2328
2329        if "intersect" in cap_strategy:
2330            trimmer.SetCappingStrategyToIntersection()
2331        elif "min" in cap_strategy:
2332            trimmer.SetCappingStrategyToMinimumDistance()
2333        elif "max" in cap_strategy:
2334            trimmer.SetCappingStrategyToMaximumDistance()
2335        elif "ave" in cap_strategy:
2336            trimmer.SetCappingStrategyToAverageDistance()
2337        else:
2338            vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}")
2339        # print (trimmer.GetCappingStrategy())
2340
2341        trimmer.Update()
2342
2343        m = Mesh(trimmer.GetOutput())
2344        m.copy_properties_from(self).flat().lighting("default")
2345        m.pipeline = OperationNode(
2346            "extrude_and_trim", parents=[self, surface],
2347            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2348        )
2349        m.name = "ExtrudedAndTrimmedMesh"
2350        return m

Extrude a Mesh and trim it with an input surface mesh.

Arguments:
  • surface : (Mesh) the surface mesh to trim with.
  • direction : (list) extrusion direction in the xy plane.
  • strategy : (str) either "boundary_edges" or "all_edges".
  • cap : (bool) enable or disable capping.
  • cap_strategy : (str) either "intersection", "minimum_distance", "maximum_distance", "average_distance".

The input Mesh is swept along a specified direction forming a "skirt" from the boundary edges 2D primitives (i.e., edges used by only one polygon); and/or from vertices and lines. The extent of the sweeping is limited by a second input: defined where the sweep intersects a user-specified surface.

Capping of the extrusion can be enabled. In this case the input, generating primitive is copied inplace as well as to the end of the extrusion skirt. (See warnings below on what happens if the intersecting sweep does not intersect, or partially intersects the trim surface.)

Note that this method operates in two fundamentally different modes based on the extrusion strategy. If the strategy is "boundary_edges", then only the boundary edges of the input's 2D primitives are extruded (verts and lines are extruded to generate lines and quads). However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).

Warning:

The extrusion direction is assumed to define an infinite line. The intersection with the trim surface is along a ray from the - to + direction, however only the first intersection is taken. Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate surfaces if capping is on and "boundary_edges" enabled, or no surface if capping is off and "boundary_edges" is enabled. If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface, then no output for that primitive will be generated. In extreme cases, it is possible that no output whatsoever will be generated.

Example:
from vedo import *
sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
circle = Circle([0,0,0], r=2, res=100).color('b6')
extruded_circle = circle.extrude_and_trim_with(
    sphere,
    direction=[0,-0.2,1],
    strategy="bound",
    cap=True,
    cap_strategy="intersection",
)
circle.lw(3).color("tomato").shift(dz=-0.1)
show(circle, sphere, extruded_circle, axes=1).close()
def split( self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True) -> List[Self]:
2352    def split(
2353        self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True
2354    ) -> List[Self]:
2355        """
2356        Split a mesh by connectivity and order the pieces by increasing area.
2357
2358        Arguments:
2359            maxdepth : (int)
2360                only consider this maximum number of mesh parts.
2361            flag : (bool)
2362                if set to True return the same single object,
2363                but add a "RegionId" array to flag the mesh subparts
2364            must_share_edge : (bool)
2365                if True, mesh regions that only share single points will be split.
2366            sort_by_area : (bool)
2367                if True, sort the mesh parts by decreasing area.
2368
2369        Examples:
2370            - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py)
2371
2372            ![](https://vedo.embl.es/images/advanced/splitmesh.png)
2373        """
2374        pd = self.dataset
2375        if must_share_edge:
2376            if pd.GetNumberOfPolys() == 0:
2377                vedo.logger.warning("in split(): no polygons found. Skip.")
2378                return [self]
2379            cf = vtki.new("PolyDataEdgeConnectivityFilter")
2380            cf.BarrierEdgesOff()
2381        else:
2382            cf = vtki.new("PolyDataConnectivityFilter")
2383
2384        cf.SetInputData(pd)
2385        cf.SetExtractionModeToAllRegions()
2386        cf.SetColorRegions(True)
2387        cf.Update()
2388        out = cf.GetOutput()
2389
2390        if not out.GetNumberOfPoints():
2391            return [self]
2392
2393        if flag:
2394            self.pipeline = OperationNode("split mesh", parents=[self])
2395            self._update(out)
2396            return [self]
2397
2398        msh = Mesh(out)
2399        if must_share_edge:
2400            arr = msh.celldata["RegionId"]
2401            on = "cells"
2402        else:
2403            arr = msh.pointdata["RegionId"]
2404            on = "points"
2405
2406        alist = []
2407        for t in range(max(arr) + 1):
2408            if t == maxdepth:
2409                break
2410            suba = msh.clone().threshold("RegionId", t, t, on=on)
2411            if sort_by_area:
2412                area = suba.area()
2413            else:
2414                area = 0  # dummy
2415            suba.name = "MeshRegion" + str(t)
2416            alist.append([suba, area])
2417
2418        if sort_by_area:
2419            alist.sort(key=lambda x: x[1])
2420            alist.reverse()
2421
2422        blist = []
2423        for i, l in enumerate(alist):
2424            l[0].color(i + 1).phong()
2425            l[0].mapper.ScalarVisibilityOff()
2426            blist.append(l[0])
2427            if i < 10:
2428                l[0].pipeline = OperationNode(
2429                    f"split mesh {i}",
2430                    parents=[self],
2431                    comment=f"#pts {l[0].dataset.GetNumberOfPoints()}",
2432                )
2433        return blist

Split a mesh by connectivity and order the pieces by increasing area.

Arguments:
  • maxdepth : (int) only consider this maximum number of mesh parts.
  • flag : (bool) if set to True return the same single object, but add a "RegionId" array to flag the mesh subparts
  • must_share_edge : (bool) if True, mesh regions that only share single points will be split.
  • sort_by_area : (bool) if True, sort the mesh parts by decreasing area.
Examples:

def extract_largest_region(self) -> Self:
2435    def extract_largest_region(self) -> Self:
2436        """
2437        Extract the largest connected part of a mesh and discard all the smaller pieces.
2438
2439        Examples:
2440            - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py)
2441        """
2442        conn = vtki.new("PolyDataConnectivityFilter")
2443        conn.SetExtractionModeToLargestRegion()
2444        conn.ScalarConnectivityOff()
2445        conn.SetInputData(self.dataset)
2446        conn.Update()
2447
2448        m = Mesh(conn.GetOutput())
2449        m.copy_properties_from(self)
2450        m.pipeline = OperationNode(
2451            "extract_largest_region",
2452            parents=[self],
2453            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2454        )
2455        m.name = "MeshLargestRegion"
2456        return m

Extract the largest connected part of a mesh and discard all the smaller pieces.

Examples:
def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self:
2458    def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self:
2459        """Volumetric union, intersection and subtraction of surfaces.
2460
2461        Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`.
2462
2463        Two possible algorithms are available.
2464        Setting `method` to 0 (the default) uses the boolean operation algorithm
2465        written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01);
2466        setting `method` to 1 will use the "loop" boolean algorithm
2467        written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015).
2468
2469        Use `tol` to specify the absolute tolerance used to determine
2470        when the distance between two points is considered to be zero (defaults to 1e-6).
2471
2472        Example:
2473            - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py)
2474
2475            ![](https://vedo.embl.es/images/basic/boolean.png)
2476        """
2477        if method == 0:
2478            bf = vtki.new("BooleanOperationPolyDataFilter")
2479        elif method == 1:
2480            bf = vtki.new("LoopBooleanPolyDataFilter")
2481        else:
2482            raise ValueError(f"Unknown method={method}")
2483
2484        poly1 = self.compute_normals().dataset
2485        poly2 = mesh2.compute_normals().dataset
2486
2487        if operation.lower() in ("plus", "+"):
2488            bf.SetOperationToUnion()
2489        elif operation.lower() == "intersect":
2490            bf.SetOperationToIntersection()
2491        elif operation.lower() in ("minus", "-"):
2492            bf.SetOperationToDifference()
2493
2494        if tol:
2495            bf.SetTolerance(tol)
2496
2497        bf.SetInputData(0, poly1)
2498        bf.SetInputData(1, poly2)
2499        bf.Update()
2500
2501        msh = Mesh(bf.GetOutput(), c=None)
2502        msh.flat()
2503
2504        msh.pipeline = OperationNode(
2505            "boolean " + operation,
2506            parents=[self, mesh2],
2507            shape="cylinder",
2508            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2509        )
2510        msh.name = self.name + operation + mesh2.name
2511        return msh

Volumetric union, intersection and subtraction of surfaces.

Use operation for the allowed operations ['plus', 'intersect', 'minus'].

Two possible algorithms are available. Setting method to 0 (the default) uses the boolean operation algorithm written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01); setting method to 1 will use the "loop" boolean algorithm written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015).

Use tol to specify the absolute tolerance used to determine when the distance between two points is considered to be zero (defaults to 1e-6).

Example:

def intersect_with(self, mesh2, tol=1e-06) -> Self:
2513    def intersect_with(self, mesh2, tol=1e-06) -> Self:
2514        """
2515        Intersect this Mesh with the input surface to return a set of lines.
2516
2517        Examples:
2518            - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py)
2519
2520                ![](https://vedo.embl.es/images/basic/surfIntersect.png)
2521        """
2522        bf = vtki.new("IntersectionPolyDataFilter")
2523        bf.SetGlobalWarningDisplay(0)
2524        bf.SetTolerance(tol)
2525        bf.SetInputData(0, self.dataset)
2526        bf.SetInputData(1, mesh2.dataset)
2527        bf.Update()
2528        msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off")
2529        msh.properties.SetLineWidth(3)
2530        msh.pipeline = OperationNode(
2531            "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}"
2532        )
2533        msh.name = "SurfaceIntersection"
2534        return msh

Intersect this Mesh with the input surface to return a set of lines.

Examples:
def intersect_with_line( self, p0, p1=None, return_ids=False, tol=0) -> Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray]]:
2536    def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
2537        """
2538        Return the list of points intersecting the mesh
2539        along the segment defined by two points `p0` and `p1`.
2540
2541        Use `return_ids` to return the cell ids along with point coords
2542
2543        Example:
2544            ```python
2545            from vedo import *
2546            s = Spring()
2547            pts = s.intersect_with_line([0,0,0], [1,0.1,0])
2548            ln = Line([0,0,0], [1,0.1,0], c='blue')
2549            ps = Points(pts, r=10, c='r')
2550            show(s, ln, ps, bg='white').close()
2551            ```
2552            ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png)
2553        """
2554        if isinstance(p0, Points):
2555            p0, p1 = p0.coordinates
2556
2557        if not self.line_locator:
2558            self.line_locator = vtki.new("OBBTree")
2559            self.line_locator.SetDataSet(self.dataset)
2560            if not tol:
2561                tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000
2562            self.line_locator.SetTolerance(tol)
2563            self.line_locator.BuildLocator()
2564
2565        vpts = vtki.vtkPoints()
2566        idlist = vtki.vtkIdList()
2567        self.line_locator.IntersectWithLine(p0, p1, vpts, idlist)
2568        pts = []
2569        for i in range(vpts.GetNumberOfPoints()):
2570            intersection: MutableSequence[float] = [0, 0, 0]
2571            vpts.GetPoint(i, intersection)
2572            pts.append(intersection)
2573        pts2 = np.array(pts)
2574
2575        if return_ids:
2576            pts_ids = []
2577            for i in range(idlist.GetNumberOfIds()):
2578                cid = idlist.GetId(i)
2579                pts_ids.append(cid)
2580            return (pts2, np.array(pts_ids).astype(np.uint32))
2581
2582        return pts2

Return the list of points intersecting the mesh along the segment defined by two points p0 and p1.

Use return_ids to return the cell ids along with point coords

Example:
from vedo import *
s = Spring()
pts = s.intersect_with_line([0,0,0], [1,0.1,0])
ln = Line([0,0,0], [1,0.1,0], c='blue')
ps = Points(pts, r=10, c='r')
show(s, ln, ps, bg='white').close()

def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
2584    def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
2585        """
2586        Intersect this Mesh with a plane to return a set of lines.
2587
2588        Example:
2589            ```python
2590            from vedo import *
2591            sph = Sphere()
2592            mi = sph.clone().intersect_with_plane().join()
2593            print(mi.lines)
2594            show(sph, mi, axes=1).close()
2595            ```
2596            ![](https://vedo.embl.es/images/feats/intersect_plane.png)
2597        """
2598        plane = vtki.new("Plane")
2599        plane.SetOrigin(origin)
2600        plane.SetNormal(normal)
2601
2602        cutter = vtki.new("PolyDataPlaneCutter")
2603        cutter.SetInputData(self.dataset)
2604        cutter.SetPlane(plane)
2605        cutter.InterpolateAttributesOn()
2606        cutter.ComputeNormalsOff()
2607        cutter.Update()
2608
2609        msh = Mesh(cutter.GetOutput())
2610        msh.c('k').lw(3).lighting("off")
2611        msh.pipeline = OperationNode(
2612            "intersect_with_plan",
2613            parents=[self],
2614            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2615        )
2616        msh.name = "PlaneIntersection"
2617        return msh

Intersect this Mesh with a plane to return a set of lines.

Example:
from vedo import *
sph = Sphere()
mi = sph.clone().intersect_with_plane().join()
print(mi.lines)
show(sph, mi, axes=1).close()

def cut_closed_surface( self, origins, normals, invert=False, return_assembly=False) -> Union[Self, vedo.assembly.Assembly]:
2619    def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]:
2620        """
2621        Cut/clip a closed surface mesh with a collection of planes.
2622        This will produce a new closed surface by creating new polygonal
2623        faces where the input surface hits the planes.
2624
2625        The orientation of the polygons that form the surface is important.
2626        Polygons have a front face and a back face, and it's the back face that defines
2627        the interior or "solid" region of the closed surface.
2628        When a plane cuts through a "solid" region, a new cut face is generated,
2629        but not when a clipping plane cuts through a hole or "empty" region.
2630        This distinction is crucial when dealing with complex surfaces.
2631        Note that if a simple surface has its back faces pointing outwards,
2632        then that surface defines a hole in a potentially infinite solid.
2633
2634        Non-manifold surfaces should not be used with this method.
2635
2636        Arguments:
2637            origins : (list)
2638                list of plane origins
2639            normals : (list)
2640                list of plane normals
2641            invert : (bool)
2642                invert the clipping.
2643            return_assembly : (bool)
2644                return the cap and the clipped surfaces as a `vedo.Assembly`.
2645
2646        Example:
2647            ```python
2648            from vedo import *
2649            s = Sphere(res=50).linewidth(1)
2650            origins = [[-0.7, 0, 0], [0, -0.6, 0]]
2651            normals = [[-1, 0, 0], [0, -1, 0]]
2652            s.cut_closed_surface(origins, normals)
2653            show(s, axes=1).close()
2654            ```
2655        """
2656        planes = vtki.new("PlaneCollection")
2657        for p, s in zip(origins, normals):
2658            plane = vtki.vtkPlane()
2659            plane.SetOrigin(vedo.utils.make3d(p))
2660            plane.SetNormal(vedo.utils.make3d(s))
2661            planes.AddItem(plane)
2662        clipper = vtki.new("ClipClosedSurface")
2663        clipper.SetInputData(self.dataset)
2664        clipper.SetClippingPlanes(planes)
2665        clipper.PassPointDataOn()
2666        clipper.GenerateFacesOn()
2667        clipper.SetScalarModeToLabels()
2668        clipper.TriangulationErrorDisplayOn()
2669        clipper.SetInsideOut(not invert)
2670
2671        if return_assembly:
2672            clipper.GenerateClipFaceOutputOn()
2673            clipper.Update()
2674            parts = []
2675            for i in range(clipper.GetNumberOfOutputPorts()):
2676                msh = Mesh(clipper.GetOutput(i))
2677                msh.copy_properties_from(self)
2678                msh.name = "CutClosedSurface"
2679                msh.pipeline = OperationNode(
2680                    "cut_closed_surface",
2681                    parents=[self],
2682                    comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2683                )
2684                parts.append(msh)
2685            asse = vedo.Assembly(parts)
2686            asse.name = "CutClosedSurface"
2687            return asse
2688
2689        else:
2690            clipper.GenerateClipFaceOutputOff()
2691            clipper.Update()
2692            self._update(clipper.GetOutput())
2693            self.flat()
2694            self.name = "CutClosedSurface"
2695            self.pipeline = OperationNode(
2696                "cut_closed_surface",
2697                parents=[self],
2698                comment=f"#pts {self.dataset.GetNumberOfPoints()}",
2699            )
2700            return self

Cut/clip a closed surface mesh with a collection of planes. This will produce a new closed surface by creating new polygonal faces where the input surface hits the planes.

The orientation of the polygons that form the surface is important. Polygons have a front face and a back face, and it's the back face that defines the interior or "solid" region of the closed surface. When a plane cuts through a "solid" region, a new cut face is generated, but not when a clipping plane cuts through a hole or "empty" region. This distinction is crucial when dealing with complex surfaces. Note that if a simple surface has its back faces pointing outwards, then that surface defines a hole in a potentially infinite solid.

Non-manifold surfaces should not be used with this method.

Arguments:
  • origins : (list) list of plane origins
  • normals : (list) list of plane normals
  • invert : (bool) invert the clipping.
  • return_assembly : (bool) return the cap and the clipped surfaces as a vedo.Assembly.
Example:
from vedo import *
s = Sphere(res=50).linewidth(1)
origins = [[-0.7, 0, 0], [0, -0.6, 0]]
normals = [[-1, 0, 0], [0, -1, 0]]
s.cut_closed_surface(origins, normals)
show(s, axes=1).close()
def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]:
2702    def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]:
2703        """
2704        Collide this Mesh with the input surface.
2705        Information is stored in `ContactCells1` and `ContactCells2`.
2706        """
2707        ipdf = vtki.new("CollisionDetectionFilter")
2708        # ipdf.SetGlobalWarningDisplay(0)
2709
2710        transform0 = vtki.vtkTransform()
2711        transform1 = vtki.vtkTransform()
2712
2713        # ipdf.SetBoxTolerance(tol)
2714        ipdf.SetCellTolerance(tol)
2715        ipdf.SetInputData(0, self.dataset)
2716        ipdf.SetInputData(1, mesh2.dataset)
2717        ipdf.SetTransform(0, transform0)
2718        ipdf.SetTransform(1, transform1)
2719        if return_bool:
2720            ipdf.SetCollisionModeToFirstContact()
2721        else:
2722            ipdf.SetCollisionModeToAllContacts()
2723        ipdf.Update()
2724
2725        if return_bool:
2726            return bool(ipdf.GetNumberOfContacts())
2727
2728        msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off")
2729        msh.metadata["ContactCells1"] = vtk2numpy(
2730            ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells")
2731        )
2732        msh.metadata["ContactCells2"] = vtk2numpy(
2733            ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells")
2734        )
2735        msh.properties.SetLineWidth(3)
2736
2737        msh.pipeline = OperationNode(
2738            "collide_with",
2739            parents=[self, mesh2],
2740            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2741        )
2742        msh.name = "SurfaceCollision"
2743        return msh

Collide this Mesh with the input surface. Information is stored in ContactCells1 and ContactCells2.

def geodesic(self, start, end) -> Self:
2745    def geodesic(self, start, end) -> Self:
2746        """
2747        Dijkstra algorithm to compute the geodesic line.
2748        Takes as input a polygonal mesh and performs a single source shortest path calculation.
2749
2750        The output mesh contains the array "VertexIDs" that contains the ordered list of vertices
2751        traversed to get from the start vertex to the end vertex.
2752
2753        Arguments:
2754            start : (int, list)
2755                start vertex index or close point `[x,y,z]`
2756            end :  (int, list)
2757                end vertex index or close point `[x,y,z]`
2758
2759        Examples:
2760            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2761
2762                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2763        """
2764        if is_sequence(start):
2765            cc = self.coordinates
2766            pa = Points(cc)
2767            start = pa.closest_point(start, return_point_id=True)
2768            end = pa.closest_point(end, return_point_id=True)
2769
2770        dijkstra = vtki.new("DijkstraGraphGeodesicPath")
2771        dijkstra.SetInputData(self.dataset)
2772        dijkstra.SetStartVertex(end)  # inverted in vtk
2773        dijkstra.SetEndVertex(start)
2774        dijkstra.Update()
2775
2776        weights = vtki.vtkDoubleArray()
2777        dijkstra.GetCumulativeWeights(weights)
2778
2779        idlist = dijkstra.GetIdList()
2780        ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())]
2781
2782        length = weights.GetMaxId() + 1
2783        arr = np.zeros(length)
2784        for i in range(length):
2785            arr[i] = weights.GetTuple(i)[0]
2786
2787        poly = dijkstra.GetOutput()
2788
2789        vdata = numpy2vtk(arr)
2790        vdata.SetName("CumulativeWeights")
2791        poly.GetPointData().AddArray(vdata)
2792
2793        vdata2 = numpy2vtk(ids, dtype=np.uint)
2794        vdata2.SetName("VertexIDs")
2795        poly.GetPointData().AddArray(vdata2)
2796        poly.GetPointData().Modified()
2797
2798        dmesh = Mesh(poly).copy_properties_from(self)
2799        dmesh.lw(3).alpha(1).lighting("off")
2800        dmesh.name = "GeodesicLine"
2801
2802        dmesh.pipeline = OperationNode(
2803            "GeodesicLine",
2804            parents=[self],
2805            comment=f"#steps {poly.GetNumberOfPoints()}",
2806        )
2807        return dmesh

Dijkstra algorithm to compute the geodesic line. Takes as input a polygonal mesh and performs a single source shortest path calculation.

The output mesh contains the array "VertexIDs" that contains the ordered list of vertices traversed to get from the start vertex to the end vertex.

Arguments:
  • start : (int, list) start vertex index or close point [x,y,z]
  • end : (int, list) end vertex index or close point [x,y,z]
Examples:
def binarize( self, values=(255, 0), spacing=None, dims=None, origin=None) -> vedo.volume.Volume:
2812    def binarize(
2813        self,
2814        values=(255, 0),
2815        spacing=None,
2816        dims=None,
2817        origin=None,
2818    ) -> "vedo.Volume":
2819        """
2820        Convert a `Mesh` into a `Volume` where
2821        the interior voxels value is set to `values[0]` (255 by default), while
2822        the exterior voxels value is set to `values[1]` (0 by default).
2823
2824        Arguments:
2825            values : (list)
2826                background and foreground values.
2827            spacing : (list)
2828                voxel spacing in x, y and z.
2829            dims : (list)
2830                dimensions (nr. of voxels) of the output volume.
2831            origin : (list)
2832                position in space of the (0,0,0) voxel.
2833
2834        Examples:
2835            - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py)
2836
2837                ![](https://vedo.embl.es/images/volumetric/mesh2volume.png)
2838        """
2839        assert len(values) == 2, "values must be a list of 2 values"
2840        fg_value, bg_value = values
2841
2842        bounds = self.bounds()
2843        if spacing is None:  # compute spacing
2844            spacing = [0, 0, 0]
2845            diagonal = np.sqrt(
2846                  (bounds[1] - bounds[0]) ** 2
2847                + (bounds[3] - bounds[2]) ** 2
2848                + (bounds[5] - bounds[4]) ** 2
2849            )
2850            spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0
2851
2852        if dims is None:  # compute dimensions
2853            dim = [0, 0, 0]
2854            for i in [0, 1, 2]:
2855                dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i]))
2856        else:
2857            dim = dims
2858
2859        white_img = vtki.vtkImageData()
2860        white_img.SetDimensions(dim)
2861        white_img.SetSpacing(spacing)
2862        white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1)
2863
2864        if origin is None:
2865            origin = [0, 0, 0]
2866            origin[0] = bounds[0] + spacing[0]
2867            origin[1] = bounds[2] + spacing[1]
2868            origin[2] = bounds[4] + spacing[2]
2869        white_img.SetOrigin(origin)
2870
2871        # if direction_matrix is not None:
2872        #     white_img.SetDirectionMatrix(direction_matrix)
2873
2874        white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
2875
2876        # fill the image with foreground voxels:
2877        white_img.GetPointData().GetScalars().Fill(fg_value)
2878
2879        # polygonal data --> image stencil:
2880        pol2stenc = vtki.new("PolyDataToImageStencil")
2881        pol2stenc.SetInputData(self.dataset)
2882        pol2stenc.SetOutputOrigin(white_img.GetOrigin())
2883        pol2stenc.SetOutputSpacing(white_img.GetSpacing())
2884        pol2stenc.SetOutputWholeExtent(white_img.GetExtent())
2885        pol2stenc.Update()
2886
2887        # cut the corresponding white image and set the background:
2888        imgstenc = vtki.new("ImageStencil")
2889        imgstenc.SetInputData(white_img)
2890        imgstenc.SetStencilConnection(pol2stenc.GetOutputPort())
2891        # imgstenc.SetReverseStencil(True)
2892        imgstenc.SetBackgroundValue(bg_value)
2893        imgstenc.Update()
2894
2895        vol = vedo.Volume(imgstenc.GetOutput())
2896        vol.name = "BinarizedVolume"
2897        vol.pipeline = OperationNode(
2898            "binarize",
2899            parents=[self],
2900            comment=f"dims={tuple(vol.dimensions())}",
2901            c="#e9c46a:#0096c7",
2902        )
2903        return vol

Convert a Mesh into a Volume where the interior voxels value is set to values[0] (255 by default), while the exterior voxels value is set to values[1] (0 by default).

Arguments:
  • values : (list) background and foreground values.
  • spacing : (list) voxel spacing in x, y and z.
  • dims : (list) dimensions (nr. of voxels) of the output volume.
  • origin : (list) position in space of the (0,0,0) voxel.
Examples:
def signed_distance( self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> vedo.volume.Volume:
2905    def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume":
2906        """
2907        Compute the `Volume` object whose voxels contains
2908        the signed distance from the mesh.
2909
2910        Arguments:
2911            bounds : (list)
2912                bounds of the output volume
2913            dims : (list)
2914                dimensions (nr. of voxels) of the output volume
2915            invert : (bool)
2916                flip the sign
2917
2918        Examples:
2919            - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py)
2920        """
2921        if maxradius is not None:
2922            vedo.logger.warning(
2923                "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)."
2924            )
2925        if bounds is None:
2926            bounds = self.bounds()
2927        sx = (bounds[1] - bounds[0]) / dims[0]
2928        sy = (bounds[3] - bounds[2]) / dims[1]
2929        sz = (bounds[5] - bounds[4]) / dims[2]
2930
2931        img = vtki.vtkImageData()
2932        img.SetDimensions(dims)
2933        img.SetSpacing(sx, sy, sz)
2934        img.SetOrigin(bounds[0], bounds[2], bounds[4])
2935        img.AllocateScalars(vtki.VTK_FLOAT, 1)
2936
2937        imp = vtki.new("ImplicitPolyDataDistance")
2938        imp.SetInput(self.dataset)
2939        b2 = bounds[2]
2940        b4 = bounds[4]
2941        d0, d1, d2 = dims
2942
2943        for i in range(d0):
2944            x = i * sx + bounds[0]
2945            for j in range(d1):
2946                y = j * sy + b2
2947                for k in range(d2):
2948                    v = imp.EvaluateFunction((x, y, k * sz + b4))
2949                    if invert:
2950                        v = -v
2951                    img.SetScalarComponentFromFloat(i, j, k, 0, v)
2952
2953        vol = vedo.Volume(img)
2954        vol.name = "SignedVolume"
2955
2956        vol.pipeline = OperationNode(
2957            "signed_distance",
2958            parents=[self],
2959            comment=f"dims={tuple(vol.dimensions())}",
2960            c="#e9c46a:#0096c7",
2961        )
2962        return vol

Compute the Volume object whose voxels contains the signed distance from the mesh.

Arguments:
  • bounds : (list) bounds of the output volume
  • dims : (list) dimensions (nr. of voxels) of the output volume
  • invert : (bool) flip the sign
Examples:
def tetralize( self, side=0.02, nmax=300000, gap=None, subsample=False, uniform=True, seed=0, debug=False) -> vedo.grids.TetMesh:
2964    def tetralize(
2965        self,
2966        side=0.02,
2967        nmax=300_000,
2968        gap=None,
2969        subsample=False,
2970        uniform=True,
2971        seed=0,
2972        debug=False,
2973    ) -> "vedo.TetMesh":
2974        """
2975        Tetralize a closed polygonal mesh. Return a `TetMesh`.
2976
2977        Arguments:
2978            side : (float)
2979                desired side of the single tetras as fraction of the bounding box diagonal.
2980                Typical values are in the range (0.01 - 0.03)
2981            nmax : (int)
2982                maximum random numbers to be sampled in the bounding box
2983            gap : (float)
2984                keep this minimum distance from the surface,
2985                if None an automatic choice is made.
2986            subsample : (bool)
2987                subsample input surface, the geometry might be affected
2988                (the number of original faces reduceed), but higher tet quality might be obtained.
2989            uniform : (bool)
2990                generate tets more uniformly packed in the interior of the mesh
2991            seed : (int)
2992                random number generator seed
2993            debug : (bool)
2994                show an intermediate plot with sampled points
2995
2996        Examples:
2997            - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py)
2998
2999                ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg)
3000        """
3001        surf = self.clone().clean().compute_normals()
3002        d = surf.diagonal_size()
3003        if gap is None:
3004            gap = side * d * np.sqrt(2 / 3)
3005        n = int(min((1 / side) ** 3, nmax))
3006
3007        # fill the space w/ points
3008        x0, x1, y0, y1, z0, z1 = surf.bounds()
3009
3010        if uniform:
3011            pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42)
3012            pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100  # some small jitter
3013        else:
3014            disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2
3015            np.random.seed(seed)
3016            pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp
3017
3018        normals = surf.celldata["Normals"]
3019        cc = surf.cell_centers().coordinates
3020        subpts = cc - normals * gap * 1.05
3021        pts = pts.tolist() + subpts.tolist()
3022
3023        if debug:
3024            print(".. tetralize(): subsampling and cleaning")
3025
3026        fillpts = surf.inside_points(pts)
3027        fillpts.subsample(side)
3028
3029        if gap:
3030            fillpts.distance_to(surf)
3031            fillpts.threshold("Distance", above=gap)
3032
3033        if subsample:
3034            surf.subsample(side)
3035
3036        merged_fs = vedo.merge(fillpts, surf)
3037        tmesh = merged_fs.generate_delaunay3d()
3038        tcenters = tmesh.cell_centers().coordinates
3039
3040        ids = surf.inside_points(tcenters, return_ids=True)
3041        ins = np.zeros(tmesh.ncells)
3042        ins[ids] = 1
3043
3044        if debug:
3045            # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close()
3046            edges = self.edges
3047            points = self.coordinates
3048            elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :])
3049            histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d))
3050            print(".. edges min, max", elen.min(), elen.max())
3051            fillpts.cmap("bone")
3052            vedo.show(
3053                [
3054                    [
3055                        f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}",
3056                        surf.wireframe().alpha(0.2),
3057                        vedo.addons.Axes(surf),
3058                        fillpts,
3059                        Points(subpts).c("r4").ps(3),
3060                    ],
3061                    [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo],
3062                ],
3063                N=2,
3064                sharecam=False,
3065                new=True,
3066            ).close()
3067            print(".. thresholding")
3068
3069        tmesh.celldata["inside"] = ins.astype(np.uint8)
3070        tmesh.threshold("inside", above=0.9)
3071        tmesh.celldata.remove("inside")
3072
3073        if debug:
3074            print(f".. tetralize() completed, ntets = {tmesh.ncells}")
3075
3076        tmesh.pipeline = OperationNode(
3077            "tetralize",
3078            parents=[self],
3079            comment=f"#tets = {tmesh.ncells}",
3080            c="#e9c46a:#9e2a2b",
3081        )
3082        return tmesh

Tetralize a closed polygonal mesh. Return a TetMesh.

Arguments:
  • side : (float) desired side of the single tetras as fraction of the bounding box diagonal. Typical values are in the range (0.01 - 0.03)
  • nmax : (int) maximum random numbers to be sampled in the bounding box
  • gap : (float) keep this minimum distance from the surface, if None an automatic choice is made.
  • subsample : (bool) subsample input surface, the geometry might be affected (the number of original faces reduceed), but higher tet quality might be obtained.
  • uniform : (bool) generate tets more uniformly packed in the interior of the mesh
  • seed : (int) random number generator seed
  • debug : (bool) show an intermediate plot with sampled points
Examples:
Inherited Members
vedo.visual.MeshVisual
follow_camera
wireframe
flat
phong
backface_culling
render_lines_as_tubes
frontface_culling
backcolor
bc
linewidth
lw
linecolor
lc
texture
vedo.pointcloud.Points
polydata
copy
clone
compute_normals_with_pca
compute_acoplanarity
distance_to
clean
subsample
threshold
quantize
point_normals
align_to
align_to_bounding_box
align_with_landmarks
normalize
mirror
flip_normals
add_gaussian_noise
closest_point
auto_distance
hausdorff_distance
chamfer_distance
remove_outliers
relax_point_positions
smooth_mls_1d
smooth_mls_2d
smooth_lloyd_2d
project_on_plane
warp
cut_with_plane
cut_with_planes
cut_with_box
cut_with_line
cut_with_cookiecutter
cut_with_cylinder
cut_with_sphere
cut_with_mesh
cut_with_point_loop
cut_with_scalar
crop
generate_surface_halo
generate_mesh
reconstruct_surface
compute_clustering
compute_connections
compute_camera_distance
densify
density
tovolume
generate_segments
generate_delaunay2d
generate_voronoi
generate_delaunay3d
visible_points
vedo.visual.PointsVisual
clone2d
copy_properties_from
color
c
alpha
lut_color_at
opacity
force_opaque
force_translucent
point_size
ps
render_points_as_spheres
lighting
point_blurring
cellcolors
pointcolors
cmap
add_trail
update_trail
add_shadow
update_shadows
labels
labels2d
legend
flagpole
flagpost
caption
vedo.visual.CommonVisual
print
LUT
scalar_range
add_observer
invoke_event
show
thumbnail
pickable
use_bounds
draggable
on
off
toggle
add_scalarbar
add_scalarbar3d
vedo.core.PointAlgorithms
apply_transform
apply_transform_from_actor
pos
shift
x
y
z
rotate
rotate_x
rotate_y
rotate_z
reorient
scale
vedo.core.CommonAlgorithms
pointdata
celldata
metadata
rename
memory_address
memory_size
modified
box
update_dataset
bounds
xbounds
ybounds
zbounds
diagonal_size
average_size
center_of_mass
copy_data_from
inputdata
npoints
nvertices
ncells
cell_centers
lines
lines_as_flat_array
mark_boundaries
find_cells_in_bounds
find_cells_along_line
find_cells_along_plane
keep_cell_types
map_cells_to_points
vertices
points
coordinates
cells_as_flat_array
cells
cell_edge_neighbors
map_points_to_cells
resample_data_from
interpolate_data_from
add_ids
gradient
divergence
vorticity
probe
compute_cell_size
generate_random_data
integrate_data
write
tomesh
unsigned_distance
smooth_data
compute_streamlines