vedo.mesh

Submodule to work with polygonal meshes

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

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

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

Initialize a Mesh object.

Arguments:
  • inputobj : (str, vtkPolyData, vtkActor, Mesh) If inputobj is None an empty mesh is created. If inputobj is 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:

def faces(self, ids=()):
250    def faces(self, ids=()):
251        """DEPRECATED. Use property `mesh.cells` instead."""
252        vedo.printc("WARNING: use property mesh.cells instead of mesh.faces()",c='y')
253        return self.cells

DEPRECATED. Use property mesh.cells instead.

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

Return an array containing the edges connectivity.

cell_normals
278    @property
279    def cell_normals(self):
280        """
281        Retrieve face normals as a numpy array.
282        Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`.
283        """
284        vtknormals = self.dataset.GetCellData().GetNormals()
285        numpy_normals = vtk2numpy(vtknormals)
286        if len(numpy_normals) == 0 and len(self.cells) != 0:
287            vedo.logger.warning(
288                "failed to return normal vectors.\n"
289                "You may need to call `Mesh.compute_normals()` before accessing 'Mesh.cell_normals'."
290            )
291            numpy_normals = np.zeros((self.ncells, 3)) + [0,0,1]
292        return numpy_normals

Retrieve face normals as a numpy array. Check out also compute_normals(cells=True) and compute_normals_with_pca().

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

Compute cell and vertex normals for the mesh.

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

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

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

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

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

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

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

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

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

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

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

def is_manifold(self) -> bool:
409    def is_manifold(self) -> bool:
410        """Return `True` if the mesh is manifold."""
411        fe = vtki.new("FeatureEdges")
412        fe.BoundaryEdgesOff()
413        fe.FeatureEdgesOff()
414        fe.NonManifoldEdgesOn()
415        fe.SetInputData(self.dataset)
416        fe.Update()
417        ne = fe.GetOutput().GetNumberOfCells()
418        return not bool(ne)

Return True if the mesh is manifold.

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

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

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

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

def genus(self) -> int:
514    def genus(self) -> int:
515        """
516        Compute the genus of the mesh.
517        The genus is a topological invariant for surfaces.
518        """
519        nb = len(self.boundaries().split()) - 1
520        return (2 - self.euler_characteristic() - nb ) / 2

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

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

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

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

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

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

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

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

Examples:

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

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

Examples:

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

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

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

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

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

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

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

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

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

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

Returns:

list of shapes.Lines

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

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

Join booundary lines by creating a triangle strip between them.

Example:

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

Split polylines into separate segments.

def remove_all_lines(self) -> Self:
848    def remove_all_lines(self) -> Self:
849        """Remove all line elements from the mesh."""
850        self.dataset.GetLines().Reset()
851        return self

Remove all line elements from the mesh.

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

Slice a mesh with a plane and fill the contour.

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

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

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

Converts mesh polygons into triangles.

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

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

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

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

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

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

def count_vertices(self) -> numpy.ndarray:
992    def count_vertices(self) -> np.ndarray:
993        """Count the number of vertices each cell has and return it as a numpy array"""
994        vc = vtki.new("CountVertices")
995        vc.SetInputData(self.dataset)
996        vc.SetOutputArrayName("VertexCount")
997        vc.Update()
998        varr = vc.GetOutput().GetCellData().GetArray("VertexCount")
999        return vtk2numpy(varr)

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

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

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

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

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

Variable method can be:

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

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

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

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

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

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

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

Increase the number of vertices of a surface mesh.

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

Downsample the number of vertices in a mesh to fraction.

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

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

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

See also: decimate_binned() and decimate_pro().

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

Downsample the number of vertices in a mesh to fraction.

This filter preserves the pointdata of the input dataset.

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

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

See also:

decimate() and decimate_binned().

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

Downsample the number of vertices in a mesh.

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

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

See also: decimate() and decimate_pro().

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

Generate n uniformly distributed random points inside the polygonal mesh.

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

Arguments:
  • n : (int) number of points to generate.
  • min_radius: (float) impose a minimum distance between points. If min_radius is set to 0, the points are generated uniformly at random inside the mesh. 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:
1467    def delete_cells(self, ids: List[int]) -> Self:
1468        """
1469        Remove cells from the mesh object by their ID.
1470        Points (vertices) are not removed (you may use `clean()` to remove those).
1471        """
1472        self.dataset.BuildLinks()
1473        for cid in ids:
1474            self.dataset.DeleteCell(cid)
1475        self.dataset.RemoveDeletedCells()
1476        self.dataset.Modified()
1477        self.mapper.Modified()
1478        self.pipeline = OperationNode(
1479            "delete_cells",
1480            parents=[self],
1481            comment=f"#cells {self.dataset.GetNumberOfCells()}",
1482        )
1483        return self

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

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

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

See also delete_cells().

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

Collapse mesh edges so that are all above distance.

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

Computes the adjacency list for mesh edge-graph.

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

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

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

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

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

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

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

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

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

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

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

Return True if point is inside a polydata closed surface.

Note:

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

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

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

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

Example:

print(pts.pointdata["IsInside"])

Examples:

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

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

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

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

Imprint the contact surface of one object onto another surface.

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

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

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

Examples:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Examples:

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

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

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

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

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

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

Warning:

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

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

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

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

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

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

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

Volumetric union, intersection and subtraction of surfaces.

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

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

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

Example:

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

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

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

Use return_ids to return the cell ids along with point coords

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Tetralize a closed polygonal mesh. Return a TetMesh.

Arguments:
  • side : (float) desired side of the single tetras as fraction of the bounding box diagonal. Typical values are in the range (0.01 - 0.03)
  • nmax : (int) maximum random numbers to be sampled in the bounding box
  • gap : (float) keep this minimum distance from the surface, if None an automatic choice is made.
  • subsample : (bool) subsample input surface, the geometry might be affected (the number of original faces reduceed), but higher tet quality might be obtained.
  • uniform : (bool) generate tets more uniformly packed in the interior of the mesh
  • seed : (int) random number generator seed
  • debug : (bool) show an intermediate plot with sampled points
Examples:
Inherited Members
vedo.visual.MeshVisual
follow_camera
wireframe
flat
phong
backface_culling
render_lines_as_tubes
frontface_culling
backcolor
bc
linewidth
lw
linecolor
lc
texture
vedo.pointcloud.Points
polydata
copy
clone
compute_normals_with_pca
compute_acoplanarity
distance_to
clean
subsample
threshold
quantize
vertex_normals
point_normals
align_to
align_to_bounding_box
align_with_landmarks
normalize
mirror
flip_normals
add_gaussian_noise
closest_point
auto_distance
hausdorff_distance
chamfer_distance
remove_outliers
relax_point_positions
smooth_mls_1d
smooth_mls_2d
smooth_lloyd_2d
project_on_plane
warp
cut_with_plane
cut_with_planes
cut_with_box
cut_with_line
cut_with_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
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