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

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:
287    def compute_normals(self, points=True, cells=True, feature_angle=None, consistency=True) -> Self:
288        """
289        Compute cell and vertex normals for the mesh.
290
291        Arguments:
292            points : (bool)
293                do the computation for the vertices too
294            cells : (bool)
295                do the computation for the cells too
296            feature_angle : (float)
297                specify the angle that defines a sharp edge.
298                If the difference in angle across neighboring polygons is greater than this value,
299                the shared edge is considered "sharp" and it is split.
300            consistency : (bool)
301                turn on/off the enforcement of consistent polygon ordering.
302
303        .. warning::
304            If `feature_angle` is set then the Mesh can be modified, and it
305            can have a different nr. of vertices from the original.
306        """
307        pdnorm = vtki.new("PolyDataNormals")
308        pdnorm.SetInputData(self.dataset)
309        pdnorm.SetComputePointNormals(points)
310        pdnorm.SetComputeCellNormals(cells)
311        pdnorm.SetConsistency(consistency)
312        pdnorm.FlipNormalsOff()
313        if feature_angle:
314            pdnorm.SetSplitting(True)
315            pdnorm.SetFeatureAngle(feature_angle)
316        else:
317            pdnorm.SetSplitting(False)
318        pdnorm.Update()
319        out = pdnorm.GetOutput()
320        self._update(out, reset_locators=False)
321        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.

def reverse(self, cells=True, normals=False) -> Self:
323    def reverse(self, cells=True, normals=False) -> Self:
324        """
325        Reverse the order of polygonal cells
326        and/or reverse the direction of point and cell normals.
327
328        Two flags are used to control these operations:
329            - `cells=True` reverses the order of the indices in the cell connectivity list.
330                If cell is a list of IDs only those cells will be reversed.
331            - `normals=True` reverses the normals by multiplying the normal vector by -1
332                (both point and cell normals, if present).
333        """
334        poly = self.dataset
335
336        if is_sequence(cells):
337            for cell in cells:
338                poly.ReverseCell(cell)
339            poly.GetCellData().Modified()
340            return self  ##############
341
342        rev = vtki.new("ReverseSense")
343        if cells:
344            rev.ReverseCellsOn()
345        else:
346            rev.ReverseCellsOff()
347        if normals:
348            rev.ReverseNormalsOn()
349        else:
350            rev.ReverseNormalsOff()
351        rev.SetInputData(poly)
352        rev.Update()
353        self._update(rev.GetOutput(), reset_locators=False)
354        self.pipeline = OperationNode("reverse", parents=[self])
355        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:
357    def volume(self) -> float:
358        """Get/set the volume occupied by mesh."""
359        mass = vtki.new("MassProperties")
360        mass.SetGlobalWarningDisplay(0)
361        mass.SetInputData(self.dataset)
362        mass.Update()
363        return mass.GetVolume()

Get/set the volume occupied by mesh.

def area(self) -> float:
365    def area(self) -> float:
366        """
367        Compute the surface area of the mesh.
368        The mesh must be triangular for this to work.
369        See also `mesh.triangulate()`.
370        """
371        mass = vtki.new("MassProperties")
372        mass.SetGlobalWarningDisplay(0)
373        mass.SetInputData(self.dataset)
374        mass.Update()
375        return mass.GetSurfaceArea()

Compute the surface area of the mesh. The mesh must be triangular for this to work. See also mesh.triangulate().

def is_closed(self) -> bool:
377    def is_closed(self) -> bool:
378        """Return `True` if the mesh is watertight."""
379        fe = vtki.new("FeatureEdges")
380        fe.BoundaryEdgesOn()
381        fe.FeatureEdgesOff()
382        fe.NonManifoldEdgesOn()
383        fe.SetInputData(self.dataset)
384        fe.Update()
385        ne = fe.GetOutput().GetNumberOfCells()
386        return not bool(ne)

Return True if the mesh is watertight.

def is_manifold(self) -> bool:
388    def is_manifold(self) -> bool:
389        """Return `True` if the mesh is manifold."""
390        fe = vtki.new("FeatureEdges")
391        fe.BoundaryEdgesOff()
392        fe.FeatureEdgesOff()
393        fe.NonManifoldEdgesOn()
394        fe.SetInputData(self.dataset)
395        fe.Update()
396        ne = fe.GetOutput().GetNumberOfCells()
397        return not bool(ne)

Return True if the mesh is manifold.

def non_manifold_faces(self, remove=True, tol='auto') -> Self:
399    def non_manifold_faces(self, remove=True, tol="auto") -> Self:
400        """
401        Detect and (try to) remove non-manifold faces of a triangular mesh:
402
403            - set `remove` to `False` to mark cells without removing them.
404            - set `tol=0` for zero-tolerance, the result will be manifold but with holes.
405            - set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
406            - set `tol="auto"` to make an automatic choice of the tolerance.
407        """
408        # mark original point and cell ids
409        self.add_ids()
410        toremove = self.boundaries(
411            boundary_edges=False,
412            non_manifold_edges=True,
413            cell_edge=True,
414            return_cell_ids=True,
415        )
416        if len(toremove) == 0: # type: ignore
417            return self
418
419        points = self.vertices
420        faces = self.cells
421        centers = self.cell_centers
422
423        copy = self.clone()
424        copy.delete_cells(toremove).clean()
425        copy.compute_normals(cells=False)
426        normals = copy.vertex_normals
427        deltas, deltas_i = [], []
428
429        for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"):
430            pids = copy.closest_point(centers[i], n=3, return_point_id=True)
431            norms = normals[pids]
432            n = np.mean(norms, axis=0)
433            dn = np.linalg.norm(n)
434            if not dn:
435                continue
436            n = n / dn
437
438            p0, p1, p2 = points[faces[i]][:3]
439            v = np.cross(p1 - p0, p2 - p0)
440            lv = np.linalg.norm(v)
441            if not lv:
442                continue
443            v = v / lv
444
445            cosa = 1 - np.dot(n, v)
446            deltas.append(cosa)
447            deltas_i.append(i)
448
449        recover = []
450        if len(deltas) > 0:
451            mean_delta = np.mean(deltas)
452            err_delta = np.std(deltas)
453            txt = ""
454            if tol == "auto":  # automatic choice
455                tol = mean_delta / 5
456                txt = f"\n Automatic tol. : {tol: .4f}"
457            for i, cosa in zip(deltas_i, deltas):
458                if cosa < tol:
459                    recover.append(i)
460
461            vedo.logger.info(
462                f"\n --------- Non manifold faces ---------"
463                f"\n Average tol.   : {mean_delta: .4f} +- {err_delta: .4f}{txt}"
464                f"\n Removed faces  : {len(toremove)}" # type: ignore
465                f"\n Recovered faces: {len(recover)}"
466            )
467
468        toremove = list(set(toremove) - set(recover)) # type: ignore
469
470        if not remove:
471            mark = np.zeros(self.ncells, dtype=np.uint8)
472            mark[recover] = 1
473            mark[toremove] = 2
474            self.celldata["NonManifoldCell"] = mark
475        else:
476            self.delete_cells(toremove) # type: ignore
477
478        self.pipeline = OperationNode(
479            "non_manifold_faces",
480            parents=[self],
481            comment=f"#cells {self.dataset.GetNumberOfCells()}",
482        )
483        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:
486    def euler_characteristic(self) -> int:
487        """
488        Compute the Euler characteristic of the mesh.
489        The Euler characteristic is a topological invariant for surfaces.
490        """
491        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:
493    def genus(self) -> int:
494        """
495        Compute the genus of the mesh.
496        The genus is a topological invariant for surfaces.
497        """
498        nb = len(self.boundaries().split()) - 1
499        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):
501    def to_reeb_graph(self, field_id=0):
502        """
503        Convert the mesh into a Reeb graph.
504        The Reeb graph is a topological structure that captures the evolution
505        of the level sets of a scalar field.
506
507        Arguments:
508            field_id : (int)
509                the id of the scalar field to use.
510        
511        Example:
512            ```python
513            from vedo import *
514            mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl")
515            mesh.rotate_x(10).rotate_y(15).alpha(0.5)
516            mesh.pointdata["scalars"] = mesh.vertices[:, 2]
517
518            printc("is_closed  :", mesh.is_closed())
519            printc("is_manifold:", mesh.is_manifold())
520            printc("euler_char :", mesh.euler_characteristic())
521            printc("genus      :", mesh.genus())
522
523            reeb = mesh.to_reeb_graph()
524            ids = reeb[0].pointdata["Vertex Ids"]
525            pts = Points(mesh.vertices[ids], r=10)
526
527            show([[mesh, pts], reeb], N=2, sharecam=False)
528            ```
529        """
530        rg = vtki.new("PolyDataToReebGraphFilter")
531        rg.SetInputData(self.dataset)
532        rg.SetFieldId(field_id)
533        rg.Update()
534        gr = vedo.pyplot.DirectedGraph()
535        gr.mdg = rg.GetOutput()
536        gr.build()
537        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.vertices[:, 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.vertices[ids], r=10)

show([[mesh, pts], reeb], N=2, sharecam=False)
def shrink(self, fraction=0.85) -> Self:
540    def shrink(self, fraction=0.85) -> Self:
541        """
542        Shrink the triangle polydata in the representation of the input mesh.
543
544        Examples:
545            - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py)
546
547            ![](https://vedo.embl.es/images/basic/shrink.png)
548        """
549        # Overriding base class method core.shrink()
550        shrink = vtki.new("ShrinkPolyData")
551        shrink.SetInputData(self.dataset)
552        shrink.SetShrinkFactor(fraction)
553        shrink.Update()
554        self._update(shrink.GetOutput())
555        self.pipeline = OperationNode("shrink", parents=[self])
556        return self

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

Examples:

def cap(self, return_cap=False) -> Self:
558    def cap(self, return_cap=False) -> Self:
559        """
560        Generate a "cap" on a clipped mesh, or caps sharp edges.
561
562        Examples:
563            - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py)
564
565            ![](https://vedo.embl.es/images/advanced/cutAndCap.png)
566
567        See also: `join()`, `join_segments()`, `slice()`.
568        """
569        fe = vtki.new("FeatureEdges")
570        fe.SetInputData(self.dataset)
571        fe.BoundaryEdgesOn()
572        fe.FeatureEdgesOff()
573        fe.NonManifoldEdgesOff()
574        fe.ManifoldEdgesOff()
575        fe.Update()
576
577        stripper = vtki.new("Stripper")
578        stripper.SetInputData(fe.GetOutput())
579        stripper.JoinContiguousSegmentsOn()
580        stripper.Update()
581
582        boundary_poly = vtki.vtkPolyData()
583        boundary_poly.SetPoints(stripper.GetOutput().GetPoints())
584        boundary_poly.SetPolys(stripper.GetOutput().GetLines())
585
586        rev = vtki.new("ReverseSense")
587        rev.ReverseCellsOn()
588        rev.SetInputData(boundary_poly)
589        rev.Update()
590
591        tf = vtki.new("TriangleFilter")
592        tf.SetInputData(rev.GetOutput())
593        tf.Update()
594
595        if return_cap:
596            m = Mesh(tf.GetOutput())
597            m.pipeline = OperationNode(
598                "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}"
599            )
600            m.name = "MeshCap"
601            return m
602
603        polyapp = vtki.new("AppendPolyData")
604        polyapp.AddInputData(self.dataset)
605        polyapp.AddInputData(tf.GetOutput())
606        polyapp.Update()
607
608        self._update(polyapp.GetOutput())
609        self.clean()
610
611        self.pipeline = OperationNode(
612            "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
613        )
614        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:
616    def join(self, polys=True, reset=False) -> Self:
617        """
618        Generate triangle strips and/or polylines from
619        input polygons, triangle strips, and lines.
620
621        Input polygons are assembled into triangle strips only if they are triangles;
622        other types of polygons are passed through to the output and not stripped.
623        Use mesh.triangulate() to triangulate non-triangular polygons prior to running
624        this filter if you need to strip all the data.
625
626        Also note that if triangle strips or polylines are present in the input
627        they are passed through and not joined nor extended.
628        If you wish to strip these use mesh.triangulate() to fragment the input
629        into triangles and lines prior to applying join().
630
631        Arguments:
632            polys : (bool)
633                polygonal segments will be joined if they are contiguous
634            reset : (bool)
635                reset points ordering
636
637        Warning:
638            If triangle strips or polylines exist in the input data
639            they will be passed through to the output data.
640            This filter will only construct triangle strips if triangle polygons
641            are available; and will only construct polylines if lines are available.
642
643        Example:
644            ```python
645            from vedo import *
646            c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate()
647            c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate()
648            intersect = c1.intersect_with(c2).join(reset=True)
649            spline = Spline(intersect).c('blue').lw(5)
650            show(c1, c2, spline, intersect.labels('id'), axes=1).close()
651            ```
652            ![](https://vedo.embl.es/images/feats/line_join.png)
653        """
654        sf = vtki.new("Stripper")
655        sf.SetPassThroughCellIds(True)
656        sf.SetPassThroughPointIds(True)
657        sf.SetJoinContiguousSegments(polys)
658        sf.SetInputData(self.dataset)
659        sf.Update()
660        if reset:
661            poly = sf.GetOutput()
662            cpd = vtki.new("CleanPolyData")
663            cpd.PointMergingOn()
664            cpd.ConvertLinesToPointsOn()
665            cpd.ConvertPolysToLinesOn()
666            cpd.ConvertStripsToPolysOn()
667            cpd.SetInputData(poly)
668            cpd.Update()
669            poly = cpd.GetOutput()
670            vpts = poly.GetCell(0).GetPoints().GetData()
671            poly.GetPoints().SetData(vpts)
672        else:
673            poly = sf.GetOutput()
674
675        self._update(poly)
676
677        self.pipeline = OperationNode(
678            "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
679        )
680        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:
682    def join_segments(self, closed=True, tol=1e-03) -> list:
683        """
684        Join line segments into contiguous lines.
685        Useful to call with `triangulate()` method.
686
687        Returns:
688            list of `shapes.Lines`
689
690        Example:
691            ```python
692            from vedo import *
693            msh = Torus().alpha(0.1).wireframe()
694            intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5')
695            slices = [s.triangulate() for s in intersection.join_segments()]
696            show(msh, intersection, merge(slices), axes=1, viewup='z')
697            ```
698            ![](https://vedo.embl.es/images/feats/join_segments.jpg)
699        """
700        vlines = []
701        for ipiece, outline in enumerate(self.split(must_share_edge=False)): # type: ignore
702
703            outline.clean()
704            pts = outline.vertices
705            if len(pts) < 3:
706                continue
707            avesize = outline.average_size()
708            lines = outline.lines
709            # print("---lines", lines, "in piece", ipiece)
710            tol = avesize / pts.shape[0] * tol
711
712            k = 0
713            joinedpts = [pts[k]]
714            for _ in range(len(pts)):
715                pk = pts[k]
716                for j, line in enumerate(lines):
717
718                    id0, id1 = line[0], line[-1]
719                    p0, p1 = pts[id0], pts[id1]
720
721                    if np.linalg.norm(p0 - pk) < tol:
722                        n = len(line)
723                        for m in range(1, n):
724                            joinedpts.append(pts[line[m]])
725                        # joinedpts.append(p1)
726                        k = id1
727                        lines.pop(j)
728                        break
729
730                    elif np.linalg.norm(p1 - pk) < tol:
731                        n = len(line)
732                        for m in reversed(range(0, n - 1)):
733                            joinedpts.append(pts[line[m]])
734                        # joinedpts.append(p0)
735                        k = id0
736                        lines.pop(j)
737                        break
738
739            if len(joinedpts) > 1:
740                newline = vedo.shapes.Line(joinedpts, closed=closed)
741                newline.clean()
742                newline.actor.SetProperty(self.properties)
743                newline.properties = self.properties
744                newline.pipeline = OperationNode(
745                    "join_segments",
746                    parents=[self],
747                    comment=f"#pts {newline.dataset.GetNumberOfPoints()}",
748                )
749                vlines.append(newline)
750
751        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:
753    def join_with_strips(self, b1, closed=True) -> Self:
754        """
755        Join booundary lines by creating a triangle strip between them.
756
757        Example:
758        ```python
759        from vedo import *
760        m1 = Cylinder(cap=False).boundaries()
761        m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
762        strips = m1.join_with_strips(m2)
763        show(m1, m2, strips, axes=1).close()
764        ```
765        """
766        b0 = self.clone().join()
767        b1 = b1.clone().join()
768
769        vertices0 = b0.vertices.tolist()
770        vertices1 = b1.vertices.tolist()
771
772        lines0 = b0.lines
773        lines1 = b1.lines
774        m =  len(lines0)
775        assert m == len(lines1), (
776            "lines must have the same number of points\n"
777            f"line has {m} points in b0 and {len(lines1)} in b1"
778        )
779
780        strips = []
781        points: List[Any] = []
782
783        for j in range(m):
784
785            ids0j = list(lines0[j])
786            ids1j = list(lines1[j])
787
788            n = len(ids0j)
789            assert n == len(ids1j), (
790                "lines must have the same number of points\n"
791                f"line {j} has {n} points in b0 and {len(ids1j)} in b1"
792            )
793
794            if closed:
795                ids0j.append(ids0j[0])
796                ids1j.append(ids1j[0])
797                vertices0.append(vertices0[ids0j[0]])
798                vertices1.append(vertices1[ids1j[0]])
799                n = n + 1
800
801            strip = []  # create a triangle strip
802            npt = len(points)
803            for ipt in range(n):
804                points.append(vertices0[ids0j[ipt]])
805                points.append(vertices1[ids1j[ipt]])
806
807            strip = list(range(npt, npt + 2*n))
808            strips.append(strip)
809
810        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:
812    def split_polylines(self) -> Self:
813        """Split polylines into separate segments."""
814        tf = vtki.new("TriangleFilter")
815        tf.SetPassLines(True)
816        tf.SetPassVerts(False)
817        tf.SetInputData(self.dataset)
818        tf.Update()
819        self._update(tf.GetOutput(), reset_locators=False)
820        self.lw(0).lighting("default").pickable()
821        self.pipeline = OperationNode(
822            "split_polylines", parents=[self], 
823            comment=f"#lines {self.dataset.GetNumberOfLines()}"
824        )
825        return self

Split polylines into separate segments.

def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
827    def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
828        """
829        Slice a mesh with a plane and fill the contour.
830
831        Example:
832            ```python
833            from vedo import *
834            msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe()
835            mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0])
836            mslice.c('purple5')
837            show(msh, mslice, axes=1)
838            ```
839            ![](https://vedo.embl.es/images/feats/mesh_slice.jpg)
840
841        See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`.
842        """
843        intersection = self.intersect_with_plane(origin=origin, normal=normal)
844        slices = [s.triangulate() for s in intersection.join_segments()]
845        mslices = vedo.pointcloud.merge(slices)
846        if mslices:
847            mslices.name = "MeshSlice"
848            mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}")
849        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:
851    def triangulate(self, verts=True, lines=True) -> Self:
852        """
853        Converts mesh polygons into triangles.
854
855        If the input mesh is only made of 2D lines (no faces) the output will be a triangulation
856        that fills the internal area. The contours may be concave, and may even contain holes,
857        i.e. a contour may contain an internal contour winding in the opposite
858        direction to indicate that it is a hole.
859
860        Arguments:
861            verts : (bool)
862                if True, break input vertex cells into individual vertex cells (one point per cell).
863                If False, the input vertex cells will be ignored.
864            lines : (bool)
865                if True, break input polylines into line segments.
866                If False, input lines will be ignored and the output will have no lines.
867        """
868        if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips():
869            # print("Using vtkTriangleFilter")
870            tf = vtki.new("TriangleFilter")
871            tf.SetPassLines(lines)
872            tf.SetPassVerts(verts)
873
874        elif self.dataset.GetNumberOfLines():
875            # print("Using vtkContourTriangulator")
876            tf = vtki.new("ContourTriangulator")
877            tf.TriangulationErrorDisplayOn()
878
879        else:
880            vedo.logger.debug("input in triangulate() seems to be void! Skip.")
881            return self
882
883        tf.SetInputData(self.dataset)
884        tf.Update()
885        self._update(tf.GetOutput(), reset_locators=False)
886        self.lw(0).lighting("default").pickable()
887
888        self.pipeline = OperationNode(
889            "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}"
890        )
891        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:
893    def compute_cell_vertex_count(self) -> Self:
894        """
895        Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.
896        """
897        csf = vtki.new("CellSizeFilter")
898        csf.SetInputData(self.dataset)
899        csf.SetComputeArea(False)
900        csf.SetComputeVolume(False)
901        csf.SetComputeLength(False)
902        csf.SetComputeVertexCount(True)
903        csf.SetVertexCountArrayName("VertexCount")
904        csf.Update()
905        self.dataset.GetCellData().AddArray(
906            csf.GetOutput().GetCellData().GetArray("VertexCount")
907        )
908        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:
910    def compute_quality(self, metric=6) -> Self:
911        """
912        Calculate metrics of quality for the elements of a triangular mesh.
913        This method adds to the mesh a cell array named "Quality".
914        See class 
915        [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html).
916
917        Arguments:
918            metric : (int)
919                type of available estimators are:
920                - EDGE RATIO, 0
921                - ASPECT RATIO, 1
922                - RADIUS RATIO, 2
923                - ASPECT FROBENIUS, 3
924                - MED ASPECT FROBENIUS, 4
925                - MAX ASPECT FROBENIUS, 5
926                - MIN_ANGLE, 6
927                - COLLAPSE RATIO, 7
928                - MAX ANGLE, 8
929                - CONDITION, 9
930                - SCALED JACOBIAN, 10
931                - SHEAR, 11
932                - RELATIVE SIZE SQUARED, 12
933                - SHAPE, 13
934                - SHAPE AND SIZE, 14
935                - DISTORTION, 15
936                - MAX EDGE RATIO, 16
937                - SKEW, 17
938                - TAPER, 18
939                - VOLUME, 19
940                - STRETCH, 20
941                - DIAGONAL, 21
942                - DIMENSION, 22
943                - ODDY, 23
944                - SHEAR AND SIZE, 24
945                - JACOBIAN, 25
946                - WARPAGE, 26
947                - ASPECT GAMMA, 27
948                - AREA, 28
949                - ASPECT BETA, 29
950
951        Examples:
952            - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py)
953
954            ![](https://vedo.embl.es/images/advanced/meshquality.png)
955        """
956        qf = vtki.new("MeshQuality")
957        qf.SetInputData(self.dataset)
958        qf.SetTriangleQualityMeasure(metric)
959        qf.SaveCellQualityOn()
960        qf.Update()
961        self._update(qf.GetOutput(), reset_locators=False)
962        self.mapper.SetScalarModeToUseCellData()
963        self.pipeline = OperationNode("compute_quality", parents=[self])
964        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:
966    def count_vertices(self) -> np.ndarray:
967        """Count the number of vertices each cell has and return it as a numpy array"""
968        vc = vtki.new("CountVertices")
969        vc.SetInputData(self.dataset)
970        vc.SetOutputArrayName("VertexCount")
971        vc.Update()
972        varr = vc.GetOutput().GetCellData().GetArray("VertexCount")
973        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:
975    def check_validity(self, tol=0) -> np.ndarray:
976        """
977        Return a numpy array of possible problematic faces following this convention:
978        - Valid               =  0
979        - WrongNumberOfPoints =  1
980        - IntersectingEdges   =  2
981        - IntersectingFaces   =  4
982        - NoncontiguousEdges  =  8
983        - Nonconvex           = 10
984        - OrientedIncorrectly = 20
985
986        Arguments:
987            tol : (float)
988                value is used as an epsilon for floating point
989                equality checks throughout the cell checking process.
990        """
991        vald = vtki.new("CellValidator")
992        if tol:
993            vald.SetTolerance(tol)
994        vald.SetInputData(self.dataset)
995        vald.Update()
996        varr = vald.GetOutput().GetCellData().GetArray("ValidityState")
997        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:
 999    def compute_curvature(self, method=0) -> Self:
1000        """
1001        Add scalars to `Mesh` that contains the curvature calculated in three different ways.
1002
1003        Variable `method` can be:
1004        - 0 = gaussian
1005        - 1 = mean curvature
1006        - 2 = max curvature
1007        - 3 = min curvature
1008
1009        Example:
1010            ```python
1011            from vedo import Torus
1012            Torus().compute_curvature().add_scalarbar().show().close()
1013            ```
1014            ![](https://vedo.embl.es/images/advanced/torus_curv.png)
1015        """
1016        curve = vtki.new("Curvatures")
1017        curve.SetInputData(self.dataset)
1018        curve.SetCurvatureType(method)
1019        curve.Update()
1020        self._update(curve.GetOutput(), reset_locators=False)
1021        self.mapper.ScalarVisibilityOn()
1022        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:
1024    def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self:
1025        """
1026        Add to `Mesh` a scalar array that contains distance along a specified direction.
1027
1028        Arguments:
1029            low : (list)
1030                one end of the line (small scalar values)
1031            high : (list)
1032                other end of the line (large scalar values)
1033            vrange : (list)
1034                set the range of the scalar
1035
1036        Example:
1037            ```python
1038            from vedo import Sphere
1039            s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1))
1040            s.add_scalarbar().show(axes=1).close()
1041            ```
1042            ![](https://vedo.embl.es/images/basic/compute_elevation.png)
1043        """
1044        ef = vtki.new("ElevationFilter")
1045        ef.SetInputData(self.dataset)
1046        ef.SetLowPoint(low)
1047        ef.SetHighPoint(high)
1048        ef.SetScalarRange(vrange)
1049        ef.Update()
1050        self._update(ef.GetOutput(), reset_locators=False)
1051        self.mapper.ScalarVisibilityOn()
1052        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 subdivide(self, n=1, method=0, mel=None) -> Self:
1054    def subdivide(self, n=1, method=0, mel=None) -> Self:
1055        """
1056        Increase the number of vertices of a surface mesh.
1057
1058        Arguments:
1059            n : (int)
1060                number of subdivisions.
1061            method : (int)
1062                Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
1063            mel : (float)
1064                Maximum Edge Length (applicable to Adaptive method only).
1065        """
1066        triangles = vtki.new("TriangleFilter")
1067        triangles.SetInputData(self.dataset)
1068        triangles.Update()
1069        tri_mesh = triangles.GetOutput()
1070        if method == 0:
1071            sdf = vtki.new("LoopSubdivisionFilter")
1072        elif method == 1:
1073            sdf = vtki.new("LinearSubdivisionFilter")
1074        elif method == 2:
1075            sdf = vtki.new("AdaptiveSubdivisionFilter")
1076            if mel is None:
1077                mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n
1078            sdf.SetMaximumEdgeLength(mel)
1079        elif method == 3:
1080            sdf = vtki.new("ButterflySubdivisionFilter")
1081        elif method == 4:
1082            sdf = vtki.new("DensifyPolyData")
1083        else:
1084            vedo.logger.error(f"in subdivide() unknown method {method}")
1085            raise RuntimeError()
1086
1087        if method != 2:
1088            sdf.SetNumberOfSubdivisions(n)
1089
1090        sdf.SetInputData(tri_mesh)
1091        sdf.Update()
1092
1093        self._update(sdf.GetOutput())
1094
1095        self.pipeline = OperationNode(
1096            "subdivide",
1097            parents=[self],
1098            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1099        )
1100        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:
1103    def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self:
1104        """
1105        Downsample the number of vertices in a mesh to `fraction`.
1106
1107        This filter preserves the `pointdata` of the input dataset. In previous versions
1108        of vedo, this decimation algorithm was referred to as quadric decimation.
1109
1110        Arguments:
1111            fraction : (float)
1112                the desired target of reduction.
1113            n : (int)
1114                the desired number of final points
1115                (`fraction` is recalculated based on it).
1116            preserve_volume : (bool)
1117                Decide whether to activate volume preservation which greatly
1118                reduces errors in triangle normal direction.
1119            regularization : (float)
1120                regularize the point finding algorithm so as to have better quality
1121                mesh elements at the cost of a slightly lower precision on the
1122                geometry potentially (mostly at sharp edges).
1123                Can be useful for decimating meshes that have been triangulated on noisy data.
1124
1125        Note:
1126            Setting `fraction=0.1` leaves 10% of the original number of vertices.
1127            Internally the VTK class
1128            [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html)
1129            is used for this operation.
1130        
1131        See also: `decimate_binned()` and `decimate_pro()`.
1132        """
1133        poly = self.dataset
1134        if n:  # N = desired number of points
1135            npt = poly.GetNumberOfPoints()
1136            fraction = n / npt
1137            if fraction >= 1:
1138                return self
1139
1140        decimate = vtki.new("QuadricDecimation")
1141        decimate.SetVolumePreservation(preserve_volume)
1142        # decimate.AttributeErrorMetricOn()
1143        if regularization:
1144            decimate.SetRegularize(True)
1145            decimate.SetRegularization(regularization)
1146
1147        try:
1148            decimate.MapPointDataOn()
1149        except AttributeError:
1150            pass
1151
1152        decimate.SetTargetReduction(1 - fraction)
1153        decimate.SetInputData(poly)
1154        decimate.Update()
1155
1156        self._update(decimate.GetOutput())
1157        self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction()
1158
1159        self.pipeline = OperationNode(
1160            "decimate",
1161            parents=[self],
1162            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1163        )
1164        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:
1166    def decimate_pro(
1167            self,
1168            fraction=0.5,
1169            n=None,
1170            preserve_topology=True,
1171            preserve_boundaries=True,
1172            splitting=False,
1173            splitting_angle=75,
1174            feature_angle=0,
1175            inflection_point_ratio=10,
1176            vertex_degree=0,
1177        ) -> Self:
1178        """
1179        Downsample the number of vertices in a mesh to `fraction`.
1180
1181        This filter preserves the `pointdata` of the input dataset.
1182
1183        Arguments:
1184            fraction : (float)
1185                The desired target of reduction.
1186                Setting `fraction=0.1` leaves 10% of the original number of vertices.
1187            n : (int)
1188                the desired number of final points (`fraction` is recalculated based on it).
1189            preserve_topology : (bool)
1190                If on, mesh splitting and hole elimination will not occur.
1191                This may limit the maximum reduction that may be achieved.
1192            preserve_boundaries : (bool)
1193                Turn on/off the deletion of vertices on the boundary of a mesh.
1194                Control whether mesh boundaries are preserved during decimation.
1195            feature_angle : (float)
1196                Specify the angle that defines a feature.
1197                This angle is used to define what an edge is
1198                (i.e., if the surface normal between two adjacent triangles
1199                is >= FeatureAngle, an edge exists).
1200            splitting : (bool)
1201                Turn on/off the splitting of the mesh at corners,
1202                along edges, at non-manifold points, or anywhere else a split is required.
1203                Turning splitting off will better preserve the original topology of the mesh,
1204                but you may not obtain the requested reduction.
1205            splitting_angle : (float)
1206                Specify the angle that defines a sharp edge.
1207                This angle is used to control the splitting of the mesh.
1208                A split line exists when the surface normals between two edge connected triangles
1209                are >= `splitting_angle`.
1210            inflection_point_ratio : (float)
1211                An inflection point occurs when the ratio of reduction error between two iterations
1212                is greater than or equal to the `inflection_point_ratio` value.
1213            vertex_degree : (int)
1214                If the number of triangles connected to a vertex exceeds it then the vertex will be split.
1215
1216        Note:
1217            Setting `fraction=0.1` leaves 10% of the original number of vertices
1218        
1219        See also:
1220            `decimate()` and `decimate_binned()`.
1221        """
1222        poly = self.dataset
1223        if n:  # N = desired number of points
1224            npt = poly.GetNumberOfPoints()
1225            fraction = n / npt
1226            if fraction >= 1:
1227                return self
1228
1229        decimate = vtki.new("DecimatePro")
1230        decimate.SetPreserveTopology(preserve_topology)
1231        decimate.SetBoundaryVertexDeletion(preserve_boundaries)
1232        if feature_angle:
1233            decimate.SetFeatureAngle(feature_angle)
1234        decimate.SetSplitting(splitting)
1235        decimate.SetSplitAngle(splitting_angle)
1236        decimate.SetInflectionPointRatio(inflection_point_ratio)
1237        if vertex_degree:
1238            decimate.SetDegree(vertex_degree)
1239
1240        decimate.SetTargetReduction(1 - fraction)
1241        decimate.SetInputData(poly)
1242        decimate.Update()
1243        self._update(decimate.GetOutput())
1244
1245        self.pipeline = OperationNode(
1246            "decimate_pro",
1247            parents=[self],
1248            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1249        )
1250        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:
1252    def decimate_binned(self, divisions=(), use_clustering=False) -> Self:
1253        """
1254        Downsample the number of vertices in a mesh.
1255        
1256        This filter preserves the `celldata` of the input dataset,
1257        if `use_clustering=True` also the `pointdata` will be preserved in the result.
1258
1259        Arguments:
1260            divisions : (list)
1261                number of divisions along x, y and z axes.
1262            auto_adjust : (bool)
1263                if True, the number of divisions is automatically adjusted to
1264                create more uniform cells.
1265            use_clustering : (bool)
1266                use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html)
1267                instead of 
1268                [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html).
1269        
1270        See also: `decimate()` and `decimate_pro()`.
1271        """
1272        if use_clustering:
1273            decimate = vtki.new("QuadricClustering")
1274            decimate.CopyCellDataOn()
1275        else:
1276            decimate = vtki.new("BinnedDecimation")
1277            decimate.ProducePointDataOn()
1278            decimate.ProduceCellDataOn()
1279
1280        decimate.SetInputData(self.dataset)
1281
1282        if len(divisions) == 0:
1283            decimate.SetAutoAdjustNumberOfDivisions(1)
1284        else:
1285            decimate.SetAutoAdjustNumberOfDivisions(0)
1286            decimate.SetNumberOfDivisions(divisions)
1287        decimate.Update()
1288
1289        self._update(decimate.GetOutput())
1290        self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions()
1291        self.pipeline = OperationNode(
1292            "decimate_binned",
1293            parents=[self],
1294            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1295        )
1296        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:
1298    def generate_random_points(self, n: int, min_radius=0.0) -> "Points":
1299        """
1300        Generate `n` uniformly distributed random points
1301        inside the polygonal mesh.
1302
1303        A new point data array is added to the output points
1304        called "OriginalCellID" which contains the index of
1305        the cell ID in which the point was generated.
1306
1307        Arguments:
1308            n : (int)
1309                number of points to generate.
1310            min_radius: (float)
1311                impose a minimum distance between points.
1312                If `min_radius` is set to 0, the points are
1313                generated uniformly at random inside the mesh.
1314                If `min_radius` is set to a positive value,
1315                the points are generated uniformly at random
1316                inside the mesh, but points closer than `min_radius`
1317                to any other point are discarded.
1318
1319        Returns a `vedo.Points` object.
1320
1321        Note:
1322            Consider using `points.probe(msh)` or
1323            `points.interpolate_data_from(msh)`
1324            to interpolate existing mesh data onto the new points.
1325
1326        Example:
1327        ```python
1328        from vedo import *
1329        msh = Mesh(dataurl + "panther.stl").lw(2)
1330        pts = msh.generate_random_points(20000, min_radius=0.5)
1331        print("Original cell ids:", pts.pointdata["OriginalCellID"])
1332        show(pts, msh, axes=1).close()
1333        ```
1334        """
1335        cmesh = self.clone().clean().triangulate().compute_cell_size()
1336        triangles = cmesh.cells
1337        vertices = cmesh.vertices
1338        cumul = np.cumsum(cmesh.celldata["Area"])
1339
1340        out_pts = []
1341        orig_cell = []
1342        for _ in range(n):
1343            # choose a triangle based on area
1344            random_area = np.random.random() * cumul[-1]
1345            it = np.searchsorted(cumul, random_area)
1346            A, B, C = vertices[triangles[it]]
1347            # calculate the random point in the triangle
1348            r1, r2 = np.random.random(2)
1349            if r1 + r2 > 1:
1350                r1 = 1 - r1
1351                r2 = 1 - r2
1352            out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C)
1353            orig_cell.append(it)
1354        nporig_cell = np.array(orig_cell, dtype=np.uint32)
1355
1356        vpts = Points(out_pts)
1357        vpts.pointdata["OriginalCellID"] = nporig_cell
1358
1359        if min_radius > 0:
1360            vpts.subsample(min_radius, absolute=True)
1361
1362        vpts.point_size(5).color("k1")
1363        vpts.name = "RandomPoints"
1364        vpts.pipeline = OperationNode(
1365            "generate_random_points", c="#edabab", parents=[self])
1366        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:
1368    def delete_cells(self, ids: List[int]) -> Self:
1369        """
1370        Remove cells from the mesh object by their ID.
1371        Points (vertices) are not removed (you may use `clean()` to remove those).
1372        """
1373        self.dataset.BuildLinks()
1374        for cid in ids:
1375            self.dataset.DeleteCell(cid)
1376        self.dataset.RemoveDeletedCells()
1377        self.dataset.Modified()
1378        self.mapper.Modified()
1379        self.pipeline = OperationNode(
1380            "delete_cells",
1381            parents=[self],
1382            comment=f"#cells {self.dataset.GetNumberOfCells()}",
1383        )
1384        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:
1386    def delete_cells_by_point_index(self, indices: List[int]) -> Self:
1387        """
1388        Delete a list of vertices identified by any of their vertex index.
1389
1390        See also `delete_cells()`.
1391
1392        Examples:
1393            - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py)
1394
1395                ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png)
1396        """
1397        cell_ids = vtki.vtkIdList()
1398        self.dataset.BuildLinks()
1399        n = 0
1400        for i in np.unique(indices):
1401            self.dataset.GetPointCells(i, cell_ids)
1402            for j in range(cell_ids.GetNumberOfIds()):
1403                self.dataset.DeleteCell(cell_ids.GetId(j))  # flag cell
1404                n += 1
1405
1406        self.dataset.RemoveDeletedCells()
1407        self.dataset.Modified()
1408        self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self])
1409        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:
1411    def collapse_edges(self, distance: float, iterations=1) -> Self:
1412        """
1413        Collapse mesh edges so that are all above `distance`.
1414        
1415        Example:
1416            ```python
1417            from vedo import *
1418            np.random.seed(2)
1419            grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
1420            grid1.celldata['scalar'] = grid1.cell_centers[:,1]
1421            grid2 = grid1.clone().collapse_edges(0.1)
1422            show(grid1, grid2, N=2, axes=1)
1423            ```
1424        """
1425        for _ in range(iterations):
1426            medges = self.edges
1427            pts = self.vertices
1428            newpts = np.array(pts)
1429            moved = []
1430            for e in medges:
1431                if len(e) == 2:
1432                    id0, id1 = e
1433                    p0, p1 = pts[id0], pts[id1]
1434                    if (np.linalg.norm(p1-p0) < distance 
1435                        and id0 not in moved
1436                        and id1 not in moved
1437                    ):
1438                        p = (p0 + p1) / 2
1439                        newpts[id0] = p
1440                        newpts[id1] = p
1441                        moved += [id0, id1]
1442            self.vertices = newpts
1443            cpd = vtki.new("CleanPolyData")
1444            cpd.ConvertLinesToPointsOff()
1445            cpd.ConvertPolysToLinesOff()
1446            cpd.ConvertStripsToPolysOff()
1447            cpd.SetInputData(self.dataset)
1448            cpd.Update()
1449            self._update(cpd.GetOutput())
1450
1451        self.pipeline = OperationNode(
1452            "collapse_edges",
1453            parents=[self],
1454            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1455        )
1456        return self

Collapse mesh edges so that are all above distance.

Example:
from vedo import *
np.random.seed(2)
grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
grid1.celldata['scalar'] = grid1.cell_centers[:,1]
grid2 = grid1.clone().collapse_edges(0.1)
show(grid1, grid2, N=2, axes=1)
def adjacency_list(self) -> List[set]:
1458    def adjacency_list(self) -> List[set]:
1459        """
1460        Computes the adjacency list for mesh edge-graph.
1461
1462        Returns: 
1463            a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex
1464        """
1465        inc = [set()] * self.nvertices
1466        for cell in self.cells:
1467            nc = len(cell)
1468            if nc > 1:
1469                for i in range(nc-1):
1470                    ci = cell[i]
1471                    inc[ci] = inc[ci].union({cell[i-1], cell[i+1]})
1472        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:
1474    def graph_ball(self, index, n: int) -> set:
1475        """
1476        Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`.
1477
1478        Arguments:
1479            index : (int)
1480                index of the vertex
1481            n : (int)
1482                radius in the graph metric
1483
1484        Returns:
1485            the set of indices of the vertices which are at most `n` edges from vertex `index`.
1486        """
1487        if n == 0:
1488            return {index}
1489        else:
1490            al = self.adjacency_list()
1491            ball = {index}
1492            i = 0
1493            while i < n and len(ball) < self.nvertices:
1494                for v in ball:
1495                    ball = ball.union(al[v])
1496                i += 1
1497            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:
1499    def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self:
1500        """
1501        Adjust mesh point positions using the so-called "Windowed Sinc" method.
1502
1503        Arguments:
1504            niter : (int)
1505                number of iterations.
1506            pass_band : (float)
1507                set the pass_band value for the windowed sinc filter.
1508            edge_angle : (float)
1509                edge angle to control smoothing along edges (either interior or boundary).
1510            feature_angle : (float)
1511                specifies the feature angle for sharp edge identification.
1512            boundary : (bool)
1513                specify if boundary should also be smoothed or kept unmodified
1514
1515        Examples:
1516            - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py)
1517
1518            ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png)
1519        """
1520        cl = vtki.new("CleanPolyData")
1521        cl.SetInputData(self.dataset)
1522        cl.Update()
1523        smf = vtki.new("WindowedSincPolyDataFilter")
1524        smf.SetInputData(cl.GetOutput())
1525        smf.SetNumberOfIterations(niter)
1526        smf.SetEdgeAngle(edge_angle)
1527        smf.SetFeatureAngle(feature_angle)
1528        smf.SetPassBand(pass_band)
1529        smf.NormalizeCoordinatesOn()
1530        smf.NonManifoldSmoothingOn()
1531        smf.FeatureEdgeSmoothingOn()
1532        smf.SetBoundarySmoothing(boundary)
1533        smf.Update()
1534
1535        self._update(smf.GetOutput())
1536
1537        self.pipeline = OperationNode(
1538            "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1539        )
1540        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:
1542    def fill_holes(self, size=None) -> Self:
1543        """
1544        Identifies and fills holes in the input mesh.
1545        Holes are identified by locating boundary edges, linking them together
1546        into loops, and then triangulating the resulting loops.
1547
1548        Arguments:
1549            size : (float)
1550                Approximate limit to the size of the hole that can be filled.
1551
1552        Examples:
1553            - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py)
1554        """
1555        fh = vtki.new("FillHolesFilter")
1556        if not size:
1557            mb = self.diagonal_size()
1558            size = mb / 10
1559        fh.SetHoleSize(size)
1560        fh.SetInputData(self.dataset)
1561        fh.Update()
1562
1563        self._update(fh.GetOutput())
1564
1565        self.pipeline = OperationNode(
1566            "fill_holes",
1567            parents=[self],
1568            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1569        )
1570        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:
1572    def contains(self, point: tuple, tol=1e-05) -> bool:
1573        """
1574        Return True if point is inside a polydata closed surface.
1575        
1576        Note:
1577            if you have many points to check use `inside_points()` instead.
1578        
1579        Example:
1580            ```python
1581            from vedo import *
1582            s = Sphere().c('green5').alpha(0.5)
1583            pt  = [0.1, 0.2, 0.3]
1584            print("Sphere contains", pt, s.contains(pt))
1585            show(s, Point(pt), axes=1).close()
1586            ```      
1587        """
1588        points = vtki.vtkPoints()
1589        points.InsertNextPoint(point)
1590        poly = vtki.vtkPolyData()
1591        poly.SetPoints(points)
1592        sep = vtki.new("SelectEnclosedPoints")
1593        sep.SetTolerance(tol)
1594        sep.CheckSurfaceOff()
1595        sep.SetInputData(poly)
1596        sep.SetSurfaceData(self.dataset)
1597        sep.Update()
1598        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]:
1600    def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]:
1601        """
1602        Return the point cloud that is inside mesh surface as a new Points object.
1603
1604        If return_ids is True a list of IDs is returned and in addition input points
1605        are marked by a pointdata array named "IsInside".
1606
1607        Example:
1608            `print(pts.pointdata["IsInside"])`
1609
1610        Examples:
1611            - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
1612
1613            ![](https://vedo.embl.es/images/basic/pca.png)
1614        """
1615        if isinstance(pts, Points):
1616            poly = pts.dataset
1617            ptsa = pts.vertices
1618        else:
1619            ptsa = np.asarray(pts)
1620            vpoints = vtki.vtkPoints()
1621            vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32))
1622            poly = vtki.vtkPolyData()
1623            poly.SetPoints(vpoints)
1624
1625        sep = vtki.new("SelectEnclosedPoints")
1626        # sep = vtki.new("ExtractEnclosedPoints()
1627        sep.SetTolerance(tol)
1628        sep.SetInputData(poly)
1629        sep.SetSurfaceData(self.dataset)
1630        sep.SetInsideOut(invert)
1631        sep.Update()
1632
1633        varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints")
1634        mask = vtk2numpy(varr).astype(bool)
1635        ids = np.array(range(len(ptsa)), dtype=int)[mask]
1636
1637        if isinstance(pts, Points):
1638            varr.SetName("IsInside")
1639            pts.dataset.GetPointData().AddArray(varr)
1640
1641        if return_ids:
1642            return ids
1643
1644        pcl = Points(ptsa[ids])
1645        pcl.name = "InsidePoints"
1646
1647        pcl.pipeline = OperationNode(
1648            "inside_points",
1649            parents=[self, ptsa],
1650            comment=f"#pts {pcl.dataset.GetNumberOfPoints()}",
1651        )
1652        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]:
1654    def boundaries(
1655        self,
1656        boundary_edges=True,
1657        manifold_edges=False,
1658        non_manifold_edges=False,
1659        feature_angle=None,
1660        return_point_ids=False,
1661        return_cell_ids=False,
1662        cell_edge=False,
1663    ) -> Union[Self, np.ndarray]:
1664        """
1665        Return the boundary lines of an input mesh.
1666        Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method.
1667
1668        Arguments:
1669            boundary_edges : (bool)
1670                Turn on/off the extraction of boundary edges.
1671            manifold_edges : (bool)
1672                Turn on/off the extraction of manifold edges.
1673            non_manifold_edges : (bool)
1674                Turn on/off the extraction of non-manifold edges.
1675            feature_angle : (bool)
1676                Specify the min angle btw 2 faces for extracting edges.
1677            return_point_ids : (bool)
1678                return a numpy array of point indices
1679            return_cell_ids : (bool)
1680                return a numpy array of cell indices
1681            cell_edge : (bool)
1682                set to `True` if a cell need to share an edge with
1683                the boundary line, or `False` if a single vertex is enough
1684
1685        Examples:
1686            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)
1687
1688            ![](https://vedo.embl.es/images/basic/boundaries.png)
1689        """
1690        fe = vtki.new("FeatureEdges")
1691        fe.SetBoundaryEdges(boundary_edges)
1692        fe.SetNonManifoldEdges(non_manifold_edges)
1693        fe.SetManifoldEdges(manifold_edges)
1694        try:
1695            fe.SetPassLines(True) # vtk9.2
1696        except AttributeError:
1697            pass
1698        fe.ColoringOff()
1699        fe.SetFeatureEdges(False)
1700        if feature_angle is not None:
1701            fe.SetFeatureEdges(True)
1702            fe.SetFeatureAngle(feature_angle)
1703
1704        if return_point_ids or return_cell_ids:
1705            idf = vtki.new("IdFilter")
1706            idf.SetInputData(self.dataset)
1707            idf.SetPointIdsArrayName("BoundaryIds")
1708            idf.SetPointIds(True)
1709            idf.Update()
1710
1711            fe.SetInputData(idf.GetOutput())
1712            fe.Update()
1713
1714            vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds")
1715            npid = vtk2numpy(vid).astype(int)
1716
1717            if return_point_ids:
1718                return npid
1719
1720            if return_cell_ids:
1721                n = 1 if cell_edge else 0
1722                inface = []
1723                for i, face in enumerate(self.cells):
1724                    # isin = np.any([vtx in npid for vtx in face])
1725                    isin = 0
1726                    for vtx in face:
1727                        isin += int(vtx in npid)
1728                        if isin > n:
1729                            break
1730                    if isin > n:
1731                        inface.append(i)
1732                return np.array(inface).astype(int)
1733
1734            return self
1735
1736        else:
1737
1738            fe.SetInputData(self.dataset)
1739            fe.Update()
1740            msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off")
1741            msh.name = "MeshBoundaries"
1742
1743            msh.pipeline = OperationNode(
1744                "boundaries",
1745                parents=[self],
1746                shape="octagon",
1747                comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
1748            )
1749            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:
1751    def imprint(self, loopline, tol=0.01) -> Self:
1752        """
1753        Imprint the contact surface of one object onto another surface.
1754
1755        Arguments:
1756            loopline : (vedo.Line)
1757                a Line object to be imprinted onto the mesh.
1758            tol : (float)
1759                projection tolerance which controls how close the imprint
1760                surface must be to the target.
1761
1762        Example:
1763            ```python
1764            from vedo import *
1765            grid = Grid()#.triangulate()
1766            circle = Circle(r=0.3, res=24).pos(0.11,0.12)
1767            line = Line(circle, closed=True, lw=4, c='r4')
1768            grid.imprint(line)
1769            show(grid, line, axes=1).close()
1770            ```
1771            ![](https://vedo.embl.es/images/feats/imprint.png)
1772        """
1773        loop = vtki.new("ContourLoopExtraction")
1774        loop.SetInputData(loopline.dataset)
1775        loop.Update()
1776
1777        clean_loop = vtki.new("CleanPolyData")
1778        clean_loop.SetInputData(loop.GetOutput())
1779        clean_loop.Update()
1780
1781        imp = vtki.new("ImprintFilter")
1782        imp.SetTargetData(self.dataset)
1783        imp.SetImprintData(clean_loop.GetOutput())
1784        imp.SetTolerance(tol)
1785        imp.BoundaryEdgeInsertionOn()
1786        imp.TriangulateOutputOn()
1787        imp.Update()
1788
1789        self._update(imp.GetOutput())
1790
1791        self.pipeline = OperationNode(
1792            "imprint",
1793            parents=[self],
1794            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1795        )
1796        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]:
1798    def connected_vertices(self, index: int) -> List[int]:
1799        """Find all vertices connected to an input vertex specified by its index.
1800
1801        Examples:
1802            - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py)
1803
1804            ![](https://vedo.embl.es/images/basic/connVtx.png)
1805        """
1806        poly = self.dataset
1807
1808        cell_idlist = vtki.vtkIdList()
1809        poly.GetPointCells(index, cell_idlist)
1810
1811        idxs = []
1812        for i in range(cell_idlist.GetNumberOfIds()):
1813            point_idlist = vtki.vtkIdList()
1814            poly.GetCellPoints(cell_idlist.GetId(i), point_idlist)
1815            for j in range(point_idlist.GetNumberOfIds()):
1816                idj = point_idlist.GetId(j)
1817                if idj == index:
1818                    continue
1819                if idj in idxs:
1820                    continue
1821                idxs.append(idj)
1822
1823        return idxs

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

Examples:

def extract_cells(self, ids: List[int]) -> Self:
1825    def extract_cells(self, ids: List[int]) -> Self:
1826        """
1827        Extract a subset of cells from a mesh and return it as a new mesh.
1828        """
1829        selectCells = vtki.new("SelectionNode")
1830        selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL)
1831        selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES)
1832        idarr = vtki.vtkIdTypeArray()
1833        idarr.SetNumberOfComponents(1)
1834        idarr.SetNumberOfValues(len(ids))
1835        for i, v in enumerate(ids):
1836            idarr.SetValue(i, v)
1837        selectCells.SetSelectionList(idarr)
1838
1839        selection = vtki.new("Selection")
1840        selection.AddNode(selectCells)
1841
1842        extractSelection = vtki.new("ExtractSelection")
1843        extractSelection.SetInputData(0, self.dataset)
1844        extractSelection.SetInputData(1, selection)
1845        extractSelection.Update()
1846
1847        gf = vtki.new("GeometryFilter")
1848        gf.SetInputData(extractSelection.GetOutput())
1849        gf.Update()
1850        msh = Mesh(gf.GetOutput())
1851        msh.copy_properties_from(self)
1852        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]]:
1854    def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]:
1855        """Find all cellls connected to an input vertex specified by its index."""
1856
1857        # Find all cells connected to point index
1858        dpoly = self.dataset
1859        idlist = vtki.vtkIdList()
1860        dpoly.GetPointCells(index, idlist)
1861
1862        ids = vtki.vtkIdTypeArray()
1863        ids.SetNumberOfComponents(1)
1864        rids = []
1865        for k in range(idlist.GetNumberOfIds()):
1866            cid = idlist.GetId(k)
1867            ids.InsertNextValue(cid)
1868            rids.append(int(cid))
1869        if return_ids:
1870            return rids
1871
1872        selection_node = vtki.new("SelectionNode")
1873        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
1874        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
1875        selection_node.SetSelectionList(ids)
1876        selection = vtki.new("Selection")
1877        selection.AddNode(selection_node)
1878        extractSelection = vtki.new("ExtractSelection")
1879        extractSelection.SetInputData(0, dpoly)
1880        extractSelection.SetInputData(1, selection)
1881        extractSelection.Update()
1882        gf = vtki.new("GeometryFilter")
1883        gf.SetInputData(extractSelection.GetOutput())
1884        gf.Update()
1885        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:
1887    def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self:
1888        """
1889        Return a new line `Mesh` which corresponds to the outer `silhouette`
1890        of the input as seen along a specified `direction`, this can also be
1891        a `vtkCamera` object.
1892
1893        Arguments:
1894            direction : (list)
1895                viewpoint direction vector.
1896                If `None` this is guessed by looking at the minimum
1897                of the sides of the bounding box.
1898            border_edges : (bool)
1899                enable or disable generation of border edges
1900            feature_angle : (float)
1901                minimal angle for sharp edges detection.
1902                If set to `False` the functionality is disabled.
1903
1904        Examples:
1905            - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py)
1906
1907            ![](https://vedo.embl.es/images/basic/silhouette1.png)
1908        """
1909        sil = vtki.new("PolyDataSilhouette")
1910        sil.SetInputData(self.dataset)
1911        sil.SetBorderEdges(border_edges)
1912        if feature_angle is False:
1913            sil.SetEnableFeatureAngle(0)
1914        else:
1915            sil.SetEnableFeatureAngle(1)
1916            sil.SetFeatureAngle(feature_angle)
1917
1918        if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera:
1919            sil.SetCamera(vedo.plotter_instance.camera)
1920            m = Mesh()
1921            m.mapper.SetInputConnection(sil.GetOutputPort())
1922
1923        elif isinstance(direction, vtki.vtkCamera):
1924            sil.SetCamera(direction)
1925            m = Mesh()
1926            m.mapper.SetInputConnection(sil.GetOutputPort())
1927
1928        elif direction == "2d":
1929            sil.SetVector(3.4, 4.5, 5.6)  # random
1930            sil.SetDirectionToSpecifiedVector()
1931            sil.Update()
1932            m = Mesh(sil.GetOutput())
1933
1934        elif is_sequence(direction):
1935            sil.SetVector(direction)
1936            sil.SetDirectionToSpecifiedVector()
1937            sil.Update()
1938            m = Mesh(sil.GetOutput())
1939        else:
1940            vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}")
1941            vedo.logger.error("first render the scene with show() or specify camera/direction")
1942            return self
1943
1944        m.lw(2).c((0, 0, 0)).lighting("off")
1945        m.mapper.SetResolveCoincidentTopologyToPolygonOffset()
1946        m.pipeline = OperationNode("silhouette", parents=[self])
1947        m.name = "Silhouette"
1948        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:
1950    def isobands(self, n=10, vmin=None, vmax=None) -> Self:
1951        """
1952        Return a new `Mesh` representing the isobands of the active scalars.
1953        This is a new mesh where the scalar is now associated to cell faces and
1954        used to colorize the mesh.
1955
1956        Arguments:
1957            n : (int)
1958                number of isobands in the range
1959            vmin : (float)
1960                minimum of the range
1961            vmax : (float)
1962                maximum of the range
1963
1964        Examples:
1965            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
1966        """
1967        r0, r1 = self.dataset.GetScalarRange()
1968        if vmin is None:
1969            vmin = r0
1970        if vmax is None:
1971            vmax = r1
1972
1973        # --------------------------------
1974        bands = []
1975        dx = (vmax - vmin) / float(n)
1976        b = [vmin, vmin + dx / 2.0, vmin + dx]
1977        i = 0
1978        while i < n:
1979            bands.append(b)
1980            b = [b[0] + dx, b[1] + dx, b[2] + dx]
1981            i += 1
1982
1983        # annotate, use the midpoint of the band as the label
1984        lut = self.mapper.GetLookupTable()
1985        labels = []
1986        for b in bands:
1987            labels.append("{:4.2f}".format(b[1]))
1988        values = vtki.vtkVariantArray()
1989        for la in labels:
1990            values.InsertNextValue(vtki.vtkVariant(la))
1991        for i in range(values.GetNumberOfTuples()):
1992            lut.SetAnnotation(i, values.GetValue(i).ToString())
1993
1994        bcf = vtki.new("BandedPolyDataContourFilter")
1995        bcf.SetInputData(self.dataset)
1996        # Use either the minimum or maximum value for each band.
1997        for i, band in enumerate(bands):
1998            bcf.SetValue(i, band[2])
1999        # We will use an indexed lookup table.
2000        bcf.SetScalarModeToIndex()
2001        bcf.GenerateContourEdgesOff()
2002        bcf.Update()
2003        bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands")
2004
2005        m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True)
2006        m1.mapper.SetLookupTable(lut)
2007        m1.mapper.SetScalarRange(lut.GetRange())
2008        m1.pipeline = OperationNode("isobands", parents=[self])
2009        m1.name = "IsoBands"
2010        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:
2012    def isolines(self, n=10, vmin=None, vmax=None) -> Self:
2013        """
2014        Return a new `Mesh` representing the isolines of the active scalars.
2015
2016        Arguments:
2017            n : (int)
2018                number of isolines in the range
2019            vmin : (float)
2020                minimum of the range
2021            vmax : (float)
2022                maximum of the range
2023
2024        Examples:
2025            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
2026
2027            ![](https://vedo.embl.es/images/pyplot/isolines.png)
2028        """
2029        bcf = vtki.new("ContourFilter")
2030        bcf.SetInputData(self.dataset)
2031        r0, r1 = self.dataset.GetScalarRange()
2032        if vmin is None:
2033            vmin = r0
2034        if vmax is None:
2035            vmax = r1
2036        bcf.GenerateValues(n, vmin, vmax)
2037        bcf.Update()
2038        sf = vtki.new("Stripper")
2039        sf.SetJoinContiguousSegments(True)
2040        sf.SetInputData(bcf.GetOutput())
2041        sf.Update()
2042        cl = vtki.new("CleanPolyData")
2043        cl.SetInputData(sf.GetOutput())
2044        cl.Update()
2045        msh = Mesh(cl.GetOutput(), c="k").lighting("off")
2046        msh.mapper.SetResolveCoincidentTopologyToPolygonOffset()
2047        msh.pipeline = OperationNode("isolines", parents=[self])
2048        msh.name = "IsoLines"
2049        return msh

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

Arguments:
  • n : (int) number of isolines in the range
  • vmin : (float) minimum of the range
  • vmax : (float) maximum of the range
Examples:

def extrude( self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self:
2051    def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self:
2052        """
2053        Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices.
2054        The input dataset is swept around the z-axis to create new polygonal primitives.
2055        For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.
2056
2057        You can control whether the sweep of a 2D object (i.e., polygon or triangle strip)
2058        is capped with the generating geometry.
2059        Also, you can control the angle of rotation, and whether translation along the z-axis
2060        is performed along with the rotation. (Translation is useful for creating "springs").
2061        You also can adjust the radius of the generating geometry using the "dR" keyword.
2062
2063        The skirt is generated by locating certain topological features.
2064        Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips)
2065        generate surfaces. This is true also of lines or polylines. Vertices generate lines.
2066
2067        This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses;
2068        or translational/rotational symmetric objects like springs or corkscrews.
2069
2070        Arguments:
2071            zshift : (float)
2072                shift along z axis.
2073            direction : (list)
2074                extrusion direction in the xy plane. 
2075                note that zshift is forced to be the 3rd component of direction,
2076                which is therefore ignored.
2077            rotation : (float)
2078                set the angle of rotation.
2079            dr : (float)
2080                set the radius variation in absolute units.
2081            cap : (bool)
2082                enable or disable capping.
2083            res : (int)
2084                set the resolution of the generating geometry.
2085
2086        Warning:
2087            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result
2088            in two separate surfaces if capping is on, or no surface if capping is off.
2089
2090        Examples:
2091            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2092
2093            ![](https://vedo.embl.es/images/basic/extrude.png)
2094        """
2095        rf = vtki.new("RotationalExtrusionFilter")
2096        # rf = vtki.new("LinearExtrusionFilter")
2097        rf.SetInputData(self.dataset)  # must not be transformed
2098        rf.SetResolution(res)
2099        rf.SetCapping(cap)
2100        rf.SetAngle(rotation)
2101        rf.SetTranslation(zshift)
2102        rf.SetDeltaRadius(dr)
2103        rf.Update()
2104
2105        # convert triangle strips to polygonal data
2106        tris = vtki.new("TriangleFilter")
2107        tris.SetInputData(rf.GetOutput())
2108        tris.Update()
2109
2110        m = Mesh(tris.GetOutput())
2111
2112        if len(direction) > 1:
2113            p = self.pos()
2114            LT = vedo.LinearTransform()
2115            LT.translate(-p)
2116            LT.concatenate([
2117                [1, 0, direction[0]],
2118                [0, 1, direction[1]],
2119                [0, 0, 1]
2120            ])
2121            LT.translate(p)
2122            m.apply_transform(LT)
2123
2124        m.copy_properties_from(self).flat().lighting("default")
2125        m.pipeline = OperationNode(
2126            "extrude", parents=[self], 
2127            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2128        )
2129        m.name = "ExtrudedMesh"
2130        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:
2132    def extrude_and_trim_with(
2133            self,
2134            surface: "Mesh",
2135            direction=(),
2136            strategy="all",
2137            cap=True,
2138            cap_strategy="max",
2139    ) -> Self:
2140        """
2141        Extrude a Mesh and trim it with an input surface mesh.
2142
2143        Arguments:
2144            surface : (Mesh)
2145                the surface mesh to trim with.
2146            direction : (list)
2147                extrusion direction in the xy plane.
2148            strategy : (str)
2149                either "boundary_edges" or "all_edges".
2150            cap : (bool)
2151                enable or disable capping.
2152            cap_strategy : (str)
2153                either "intersection", "minimum_distance", "maximum_distance", "average_distance".
2154
2155        The input Mesh is swept along a specified direction forming a "skirt"
2156        from the boundary edges 2D primitives (i.e., edges used by only one polygon);
2157        and/or from vertices and lines.
2158        The extent of the sweeping is limited by a second input: defined where
2159        the sweep intersects a user-specified surface.
2160
2161        Capping of the extrusion can be enabled.
2162        In this case the input, generating primitive is copied inplace as well
2163        as to the end of the extrusion skirt.
2164        (See warnings below on what happens if the intersecting sweep does not
2165        intersect, or partially intersects the trim surface.)
2166
2167        Note that this method operates in two fundamentally different modes
2168        based on the extrusion strategy. 
2169        If the strategy is "boundary_edges", then only the boundary edges of the input's
2170        2D primitives are extruded (verts and lines are extruded to generate lines and quads).
2171        However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives
2172        is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).
2173
2174        Warning:
2175            The extrusion direction is assumed to define an infinite line.
2176            The intersection with the trim surface is along a ray from the - to + direction,
2177            however only the first intersection is taken.
2178            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate
2179            surfaces if capping is on and "boundary_edges" enabled,
2180            or no surface if capping is off and "boundary_edges" is enabled.
2181            If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface,
2182            then no output for that primitive will be generated. In extreme cases, it is possible that no output
2183            whatsoever will be generated.
2184        
2185        Example:
2186            ```python
2187            from vedo import *
2188            sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
2189            circle = Circle([0,0,0], r=2, res=100).color('b6')
2190            extruded_circle = circle.extrude_and_trim_with(
2191                sphere, 
2192                direction=[0,-0.2,1],
2193                strategy="bound",
2194                cap=True,
2195                cap_strategy="intersection",
2196            )
2197            circle.lw(3).color("tomato").shift(dz=-0.1)
2198            show(circle, sphere, extruded_circle, axes=1).close()
2199            ```
2200        """
2201        trimmer = vtki.new("TrimmedExtrusionFilter")
2202        trimmer.SetInputData(self.dataset)
2203        trimmer.SetCapping(cap)
2204        trimmer.SetExtrusionDirection(direction)
2205        trimmer.SetTrimSurfaceData(surface.dataset)
2206        if "bound" in strategy:
2207            trimmer.SetExtrusionStrategyToBoundaryEdges()
2208        elif "all" in strategy:
2209            trimmer.SetExtrusionStrategyToAllEdges()
2210        else:
2211            vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}")
2212        # print (trimmer.GetExtrusionStrategy())
2213        
2214        if "intersect" in cap_strategy:
2215            trimmer.SetCappingStrategyToIntersection()
2216        elif "min" in cap_strategy:
2217            trimmer.SetCappingStrategyToMinimumDistance()
2218        elif "max" in cap_strategy:
2219            trimmer.SetCappingStrategyToMaximumDistance()
2220        elif "ave" in cap_strategy:
2221            trimmer.SetCappingStrategyToAverageDistance()
2222        else:
2223            vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}")
2224        # print (trimmer.GetCappingStrategy())
2225
2226        trimmer.Update()
2227
2228        m = Mesh(trimmer.GetOutput())
2229        m.copy_properties_from(self).flat().lighting("default")
2230        m.pipeline = OperationNode(
2231            "extrude_and_trim", parents=[self, surface],
2232            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2233        )
2234        m.name = "ExtrudedAndTrimmedMesh"
2235        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) -> Union[List[Self], Self]:
2237    def split(
2238        self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True
2239    ) -> Union[List[Self], Self]:
2240        """
2241        Split a mesh by connectivity and order the pieces by increasing area.
2242
2243        Arguments:
2244            maxdepth : (int)
2245                only consider this maximum number of mesh parts.
2246            flag : (bool)
2247                if set to True return the same single object,
2248                but add a "RegionId" array to flag the mesh subparts
2249            must_share_edge : (bool)
2250                if True, mesh regions that only share single points will be split.
2251            sort_by_area : (bool)
2252                if True, sort the mesh parts by decreasing area.
2253
2254        Examples:
2255            - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py)
2256
2257            ![](https://vedo.embl.es/images/advanced/splitmesh.png)
2258        """
2259        pd = self.dataset
2260        if must_share_edge:
2261            if pd.GetNumberOfPolys() == 0:
2262                vedo.logger.warning("in split(): no polygons found. Skip.")
2263                return [self]
2264            cf = vtki.new("PolyDataEdgeConnectivityFilter")
2265            cf.BarrierEdgesOff()
2266        else:
2267            cf = vtki.new("PolyDataConnectivityFilter")
2268
2269        cf.SetInputData(pd)
2270        cf.SetExtractionModeToAllRegions()
2271        cf.SetColorRegions(True)
2272        cf.Update()
2273        out = cf.GetOutput()
2274
2275        if not out.GetNumberOfPoints():
2276            return [self]
2277
2278        if flag:
2279            self.pipeline = OperationNode("split mesh", parents=[self])
2280            self._update(out)
2281            return self
2282
2283        msh = Mesh(out)
2284        if must_share_edge:
2285            arr = msh.celldata["RegionId"]
2286            on = "cells"
2287        else:
2288            arr = msh.pointdata["RegionId"]
2289            on = "points"
2290
2291        alist = []
2292        for t in range(max(arr) + 1):
2293            if t == maxdepth:
2294                break
2295            suba = msh.clone().threshold("RegionId", t, t, on=on)
2296            if sort_by_area:
2297                area = suba.area()
2298            else:
2299                area = 0  # dummy
2300            suba.name = "MeshRegion" + str(t)
2301            alist.append([suba, area])
2302
2303        if sort_by_area:
2304            alist.sort(key=lambda x: x[1])
2305            alist.reverse()
2306
2307        blist = []
2308        for i, l in enumerate(alist):
2309            l[0].color(i + 1).phong()
2310            l[0].mapper.ScalarVisibilityOff()
2311            blist.append(l[0])
2312            if i < 10:
2313                l[0].pipeline = OperationNode(
2314                    f"split mesh {i}",
2315                    parents=[self],
2316                    comment=f"#pts {l[0].dataset.GetNumberOfPoints()}",
2317                )
2318        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:
2320    def extract_largest_region(self) -> Self:
2321        """
2322        Extract the largest connected part of a mesh and discard all the smaller pieces.
2323
2324        Examples:
2325            - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py)
2326        """
2327        conn = vtki.new("PolyDataConnectivityFilter")
2328        conn.SetExtractionModeToLargestRegion()
2329        conn.ScalarConnectivityOff()
2330        conn.SetInputData(self.dataset)
2331        conn.Update()
2332
2333        m = Mesh(conn.GetOutput())
2334        m.copy_properties_from(self)
2335        m.pipeline = OperationNode(
2336            "extract_largest_region",
2337            parents=[self],
2338            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2339        )
2340        m.name = "MeshLargestRegion"
2341        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:
2343    def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self:
2344        """Volumetric union, intersection and subtraction of surfaces.
2345
2346        Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`.
2347
2348        Two possible algorithms are available by changing `method`.
2349
2350        Example:
2351            - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py)
2352
2353            ![](https://vedo.embl.es/images/basic/boolean.png)
2354        """
2355        if method == 0:
2356            bf = vtki.new("BooleanOperationPolyDataFilter")
2357        elif method == 1:
2358            bf = vtki.new("LoopBooleanPolyDataFilter")
2359        else:
2360            raise ValueError(f"Unknown method={method}")
2361
2362        poly1 = self.compute_normals().dataset
2363        poly2 = mesh2.compute_normals().dataset
2364
2365        if operation.lower() in ("plus", "+"):
2366            bf.SetOperationToUnion()
2367        elif operation.lower() == "intersect":
2368            bf.SetOperationToIntersection()
2369        elif operation.lower() in ("minus", "-"):
2370            bf.SetOperationToDifference()
2371
2372        if tol:
2373            bf.SetTolerance(tol)
2374
2375        bf.SetInputData(0, poly1)
2376        bf.SetInputData(1, poly2)
2377        bf.Update()
2378
2379        msh = Mesh(bf.GetOutput(), c=None)
2380        msh.flat()
2381
2382        msh.pipeline = OperationNode(
2383            "boolean " + operation,
2384            parents=[self, mesh2],
2385            shape="cylinder",
2386            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2387        )
2388        msh.name = self.name + operation + mesh2.name
2389        return msh

Volumetric union, intersection and subtraction of surfaces.

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

Two possible algorithms are available by changing method.

Example:

def intersect_with(self, mesh2, tol=1e-06) -> Self:
2391    def intersect_with(self, mesh2, tol=1e-06) -> Self:
2392        """
2393        Intersect this Mesh with the input surface to return a set of lines.
2394
2395        Examples:
2396            - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py)
2397
2398                ![](https://vedo.embl.es/images/basic/surfIntersect.png)
2399        """
2400        bf = vtki.new("IntersectionPolyDataFilter")
2401        bf.SetGlobalWarningDisplay(0)
2402        bf.SetTolerance(tol)
2403        bf.SetInputData(0, self.dataset)
2404        bf.SetInputData(1, mesh2.dataset)
2405        bf.Update()
2406        msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off")
2407        msh.properties.SetLineWidth(3)
2408        msh.pipeline = OperationNode(
2409            "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}"
2410        )
2411        msh.name = "SurfaceIntersection"
2412        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]]:
2414    def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
2415        """
2416        Return the list of points intersecting the mesh
2417        along the segment defined by two points `p0` and `p1`.
2418
2419        Use `return_ids` to return the cell ids along with point coords
2420
2421        Example:
2422            ```python
2423            from vedo import *
2424            s = Spring()
2425            pts = s.intersect_with_line([0,0,0], [1,0.1,0])
2426            ln = Line([0,0,0], [1,0.1,0], c='blue')
2427            ps = Points(pts, r=10, c='r')
2428            show(s, ln, ps, bg='white').close()
2429            ```
2430            ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png)
2431        """
2432        if isinstance(p0, Points):
2433            p0, p1 = p0.vertices
2434
2435        if not self.line_locator:
2436            self.line_locator = vtki.new("OBBTree")
2437            self.line_locator.SetDataSet(self.dataset)
2438            if not tol:
2439                tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000
2440            self.line_locator.SetTolerance(tol)
2441            self.line_locator.BuildLocator()
2442
2443        vpts = vtki.vtkPoints()
2444        idlist = vtki.vtkIdList()
2445        self.line_locator.IntersectWithLine(p0, p1, vpts, idlist)
2446        pts = []
2447        for i in range(vpts.GetNumberOfPoints()):
2448            intersection: MutableSequence[float] = [0, 0, 0]
2449            vpts.GetPoint(i, intersection)
2450            pts.append(intersection)
2451        pts2 = np.array(pts)
2452
2453        if return_ids:
2454            pts_ids = []
2455            for i in range(idlist.GetNumberOfIds()):
2456                cid = idlist.GetId(i)
2457                pts_ids.append(cid)
2458            return (pts2, np.array(pts_ids).astype(np.uint32))
2459
2460        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:
2462    def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
2463        """
2464        Intersect this Mesh with a plane to return a set of lines.
2465
2466        Example:
2467            ```python
2468            from vedo import *
2469            sph = Sphere()
2470            mi = sph.clone().intersect_with_plane().join()
2471            print(mi.lines)
2472            show(sph, mi, axes=1).close()
2473            ```
2474            ![](https://vedo.embl.es/images/feats/intersect_plane.png)
2475        """
2476        plane = vtki.new("Plane")
2477        plane.SetOrigin(origin)
2478        plane.SetNormal(normal)
2479
2480        cutter = vtki.new("PolyDataPlaneCutter")
2481        cutter.SetInputData(self.dataset)
2482        cutter.SetPlane(plane)
2483        cutter.InterpolateAttributesOn()
2484        cutter.ComputeNormalsOff()
2485        cutter.Update()
2486
2487        msh = Mesh(cutter.GetOutput())
2488        msh.c('k').lw(3).lighting("off")
2489        msh.pipeline = OperationNode(
2490            "intersect_with_plan",
2491            parents=[self],
2492            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2493        )
2494        msh.name = "PlaneIntersection"
2495        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]:
2497    def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]:
2498        """
2499        Cut/clip a closed surface mesh with a collection of planes.
2500        This will produce a new closed surface by creating new polygonal
2501        faces where the input surface hits the planes.
2502
2503        The orientation of the polygons that form the surface is important.
2504        Polygons have a front face and a back face, and it's the back face that defines
2505        the interior or "solid" region of the closed surface.
2506        When a plane cuts through a "solid" region, a new cut face is generated,
2507        but not when a clipping plane cuts through a hole or "empty" region.
2508        This distinction is crucial when dealing with complex surfaces.
2509        Note that if a simple surface has its back faces pointing outwards,
2510        then that surface defines a hole in a potentially infinite solid.
2511
2512        Non-manifold surfaces should not be used with this method. 
2513
2514        Arguments:
2515            origins : (list)
2516                list of plane origins
2517            normals : (list)
2518                list of plane normals
2519            invert : (bool)
2520                invert the clipping.
2521            return_assembly : (bool)
2522                return the cap and the clipped surfaces as a `vedo.Assembly`.
2523        
2524        Example:
2525            ```python
2526            from vedo import *
2527            s = Sphere(res=50).linewidth(1)
2528            origins = [[-0.7, 0, 0], [0, -0.6, 0]]
2529            normals = [[-1, 0, 0], [0, -1, 0]]
2530            s.cut_closed_surface(origins, normals)
2531            show(s, axes=1).close()
2532            ```
2533        """        
2534        planes = vtki.new("PlaneCollection")
2535        for p, s in zip(origins, normals):
2536            plane = vtki.vtkPlane()
2537            plane.SetOrigin(vedo.utils.make3d(p))
2538            plane.SetNormal(vedo.utils.make3d(s))
2539            planes.AddItem(plane)
2540        clipper = vtki.new("ClipClosedSurface")
2541        clipper.SetInputData(self.dataset)
2542        clipper.SetClippingPlanes(planes)
2543        clipper.PassPointDataOn()
2544        clipper.GenerateFacesOn()
2545        clipper.SetScalarModeToLabels()
2546        clipper.TriangulationErrorDisplayOn()
2547        clipper.SetInsideOut(not invert)
2548
2549        if return_assembly:
2550            clipper.GenerateClipFaceOutputOn()
2551            clipper.Update()
2552            parts = []
2553            for i in range(clipper.GetNumberOfOutputPorts()):
2554                msh = Mesh(clipper.GetOutput(i))
2555                msh.copy_properties_from(self)
2556                msh.name = "CutClosedSurface"
2557                msh.pipeline = OperationNode(
2558                    "cut_closed_surface",
2559                    parents=[self],
2560                    comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2561                )
2562                parts.append(msh)
2563            asse = vedo.Assembly(parts)
2564            asse.name = "CutClosedSurface"
2565            return asse
2566
2567        else:
2568            clipper.GenerateClipFaceOutputOff()
2569            clipper.Update()
2570            self._update(clipper.GetOutput())
2571            self.flat()
2572            self.name = "CutClosedSurface"
2573            self.pipeline = OperationNode(
2574                "cut_closed_surface",
2575                parents=[self],
2576                comment=f"#pts {self.dataset.GetNumberOfPoints()}",
2577            )
2578            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]:
2580    def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]:
2581        """
2582        Collide this Mesh with the input surface.
2583        Information is stored in `ContactCells1` and `ContactCells2`.
2584        """
2585        ipdf = vtki.new("CollisionDetectionFilter")
2586        # ipdf.SetGlobalWarningDisplay(0)
2587
2588        transform0 = vtki.vtkTransform()
2589        transform1 = vtki.vtkTransform()
2590
2591        # ipdf.SetBoxTolerance(tol)
2592        ipdf.SetCellTolerance(tol)
2593        ipdf.SetInputData(0, self.dataset)
2594        ipdf.SetInputData(1, mesh2.dataset)
2595        ipdf.SetTransform(0, transform0)
2596        ipdf.SetTransform(1, transform1)
2597        if return_bool:
2598            ipdf.SetCollisionModeToFirstContact()
2599        else:
2600            ipdf.SetCollisionModeToAllContacts()
2601        ipdf.Update()
2602
2603        if return_bool:
2604            return bool(ipdf.GetNumberOfContacts())
2605
2606        msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off")
2607        msh.metadata["ContactCells1"] = vtk2numpy(
2608            ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells")
2609        )
2610        msh.metadata["ContactCells2"] = vtk2numpy(
2611            ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells")
2612        )
2613        msh.properties.SetLineWidth(3)
2614
2615        msh.pipeline = OperationNode(
2616            "collide_with",
2617            parents=[self, mesh2],
2618            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2619        )
2620        msh.name = "SurfaceCollision"
2621        return msh

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

def geodesic(self, start, end) -> Self:
2623    def geodesic(self, start, end) -> Self:
2624        """
2625        Dijkstra algorithm to compute the geodesic line.
2626        Takes as input a polygonal mesh and performs a single source shortest path calculation.
2627
2628        The output mesh contains the array "VertexIDs" that contains the ordered list of vertices
2629        traversed to get from the start vertex to the end vertex.
2630        
2631        Arguments:
2632            start : (int, list)
2633                start vertex index or close point `[x,y,z]`
2634            end :  (int, list)
2635                end vertex index or close point `[x,y,z]`
2636
2637        Examples:
2638            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2639
2640                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2641        """
2642        if is_sequence(start):
2643            cc = self.vertices
2644            pa = Points(cc)
2645            start = pa.closest_point(start, return_point_id=True)
2646            end = pa.closest_point(end, return_point_id=True)
2647
2648        dijkstra = vtki.new("DijkstraGraphGeodesicPath")
2649        dijkstra.SetInputData(self.dataset)
2650        dijkstra.SetStartVertex(end)  # inverted in vtk
2651        dijkstra.SetEndVertex(start)
2652        dijkstra.Update()
2653
2654        weights = vtki.vtkDoubleArray()
2655        dijkstra.GetCumulativeWeights(weights)
2656
2657        idlist = dijkstra.GetIdList()
2658        ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())]
2659
2660        length = weights.GetMaxId() + 1
2661        arr = np.zeros(length)
2662        for i in range(length):
2663            arr[i] = weights.GetTuple(i)[0]
2664
2665        poly = dijkstra.GetOutput()
2666
2667        vdata = numpy2vtk(arr)
2668        vdata.SetName("CumulativeWeights")
2669        poly.GetPointData().AddArray(vdata)
2670
2671        vdata2 = numpy2vtk(ids, dtype=np.uint)
2672        vdata2.SetName("VertexIDs")
2673        poly.GetPointData().AddArray(vdata2)
2674        poly.GetPointData().Modified()
2675
2676        dmesh = Mesh(poly).copy_properties_from(self)
2677        dmesh.lw(3).alpha(1).lighting("off")
2678        dmesh.name = "GeodesicLine"
2679
2680        dmesh.pipeline = OperationNode(
2681            "GeodesicLine",
2682            parents=[self],
2683            comment=f"#steps {poly.GetNumberOfPoints()}",
2684        )
2685        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:
2690    def binarize(
2691        self,
2692        values=(255, 0),
2693        spacing=None,
2694        dims=None,
2695        origin=None,
2696    ) -> "vedo.Volume":
2697        """
2698        Convert a `Mesh` into a `Volume` where
2699        the interior voxels value is set to `values[0]` (255 by default), while
2700        the exterior voxels value is set to `values[1]` (0 by default).
2701
2702        Arguments:
2703            values : (list)
2704                background and foreground values.
2705            spacing : (list)
2706                voxel spacing in x, y and z.
2707            dims : (list)
2708                dimensions (nr. of voxels) of the output volume.
2709            origin : (list)
2710                position in space of the (0,0,0) voxel.
2711
2712        Examples:
2713            - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py)
2714
2715                ![](https://vedo.embl.es/images/volumetric/mesh2volume.png)
2716        """
2717        assert len(values) == 2, "values must be a list of 2 values"
2718        fg_value, bg_value = values
2719
2720        bounds = self.bounds()
2721        if spacing is None:  # compute spacing
2722            spacing = [0, 0, 0]
2723            diagonal = np.sqrt(
2724                  (bounds[1] - bounds[0]) ** 2
2725                + (bounds[3] - bounds[2]) ** 2
2726                + (bounds[5] - bounds[4]) ** 2
2727            )
2728            spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0
2729
2730        if dims is None:  # compute dimensions
2731            dim = [0, 0, 0]
2732            for i in [0, 1, 2]:
2733                dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i]))
2734        else:
2735            dim = dims
2736        
2737        white_img = vtki.vtkImageData()
2738        white_img.SetDimensions(dim)
2739        white_img.SetSpacing(spacing)
2740        white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1)
2741
2742        if origin is None:
2743            origin = [0, 0, 0]
2744            origin[0] = bounds[0] + spacing[0]
2745            origin[1] = bounds[2] + spacing[1]
2746            origin[2] = bounds[4] + spacing[2]
2747        white_img.SetOrigin(origin)
2748
2749        # if direction_matrix is not None:
2750        #     white_img.SetDirectionMatrix(direction_matrix)
2751
2752        white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
2753
2754        # fill the image with foreground voxels:
2755        white_img.GetPointData().GetScalars().Fill(fg_value)
2756
2757        # polygonal data --> image stencil:
2758        pol2stenc = vtki.new("PolyDataToImageStencil")
2759        pol2stenc.SetInputData(self.dataset)
2760        pol2stenc.SetOutputOrigin(white_img.GetOrigin())
2761        pol2stenc.SetOutputSpacing(white_img.GetSpacing())
2762        pol2stenc.SetOutputWholeExtent(white_img.GetExtent())
2763        pol2stenc.Update()
2764
2765        # cut the corresponding white image and set the background:
2766        imgstenc = vtki.new("ImageStencil")
2767        imgstenc.SetInputData(white_img)
2768        imgstenc.SetStencilConnection(pol2stenc.GetOutputPort())
2769        # imgstenc.SetReverseStencil(True)
2770        imgstenc.SetBackgroundValue(bg_value)
2771        imgstenc.Update()
2772
2773        vol = vedo.Volume(imgstenc.GetOutput())
2774        vol.name = "BinarizedVolume"
2775        vol.pipeline = OperationNode(
2776            "binarize",
2777            parents=[self],
2778            comment=f"dims={tuple(vol.dimensions())}",
2779            c="#e9c46a:#0096c7",
2780        )
2781        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:
2783    def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume":
2784        """
2785        Compute the `Volume` object whose voxels contains 
2786        the signed distance from the mesh.
2787
2788        Arguments:
2789            bounds : (list)
2790                bounds of the output volume
2791            dims : (list)
2792                dimensions (nr. of voxels) of the output volume
2793            invert : (bool)
2794                flip the sign
2795
2796        Examples:
2797            - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py)
2798        """
2799        if maxradius is not None:
2800            vedo.logger.warning(
2801                "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)."
2802            )
2803        if bounds is None:
2804            bounds = self.bounds()
2805        sx = (bounds[1] - bounds[0]) / dims[0]
2806        sy = (bounds[3] - bounds[2]) / dims[1]
2807        sz = (bounds[5] - bounds[4]) / dims[2]
2808
2809        img = vtki.vtkImageData()
2810        img.SetDimensions(dims)
2811        img.SetSpacing(sx, sy, sz)
2812        img.SetOrigin(bounds[0], bounds[2], bounds[4])
2813        img.AllocateScalars(vtki.VTK_FLOAT, 1)
2814
2815        imp = vtki.new("ImplicitPolyDataDistance")
2816        imp.SetInput(self.dataset)
2817        b2 = bounds[2]
2818        b4 = bounds[4]
2819        d0, d1, d2 = dims
2820
2821        for i in range(d0):
2822            x = i * sx + bounds[0]
2823            for j in range(d1):
2824                y = j * sy + b2
2825                for k in range(d2):
2826                    v = imp.EvaluateFunction((x, y, k * sz + b4))
2827                    if invert:
2828                        v = -v
2829                    img.SetScalarComponentFromFloat(i, j, k, 0, v)
2830
2831        vol = vedo.Volume(img)
2832        vol.name = "SignedVolume"
2833
2834        vol.pipeline = OperationNode(
2835            "signed_distance",
2836            parents=[self],
2837            comment=f"dims={tuple(vol.dimensions())}",
2838            c="#e9c46a:#0096c7",
2839        )
2840        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:
2842    def tetralize(
2843        self,
2844        side=0.02,
2845        nmax=300_000,
2846        gap=None,
2847        subsample=False,
2848        uniform=True,
2849        seed=0,
2850        debug=False,
2851    ) -> "vedo.TetMesh":
2852        """
2853        Tetralize a closed polygonal mesh. Return a `TetMesh`.
2854
2855        Arguments:
2856            side : (float)
2857                desired side of the single tetras as fraction of the bounding box diagonal.
2858                Typical values are in the range (0.01 - 0.03)
2859            nmax : (int)
2860                maximum random numbers to be sampled in the bounding box
2861            gap : (float)
2862                keep this minimum distance from the surface,
2863                if None an automatic choice is made.
2864            subsample : (bool)
2865                subsample input surface, the geometry might be affected
2866                (the number of original faces reduceed), but higher tet quality might be obtained.
2867            uniform : (bool)
2868                generate tets more uniformly packed in the interior of the mesh
2869            seed : (int)
2870                random number generator seed
2871            debug : (bool)
2872                show an intermediate plot with sampled points
2873
2874        Examples:
2875            - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py)
2876
2877                ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg)
2878        """
2879        surf = self.clone().clean().compute_normals()
2880        d = surf.diagonal_size()
2881        if gap is None:
2882            gap = side * d * np.sqrt(2 / 3)
2883        n = int(min((1 / side) ** 3, nmax))
2884
2885        # fill the space w/ points
2886        x0, x1, y0, y1, z0, z1 = surf.bounds()
2887
2888        if uniform:
2889            pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42)
2890            pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100  # some small jitter
2891        else:
2892            disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2
2893            np.random.seed(seed)
2894            pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp
2895
2896        normals = surf.celldata["Normals"]
2897        cc = surf.cell_centers
2898        subpts = cc - normals * gap * 1.05
2899        pts = pts.tolist() + subpts.tolist()
2900
2901        if debug:
2902            print(".. tetralize(): subsampling and cleaning")
2903
2904        fillpts = surf.inside_points(pts)
2905        fillpts.subsample(side)
2906
2907        if gap:
2908            fillpts.distance_to(surf)
2909            fillpts.threshold("Distance", above=gap)
2910
2911        if subsample:
2912            surf.subsample(side)
2913
2914        merged_fs = vedo.merge(fillpts, surf)
2915        tmesh = merged_fs.generate_delaunay3d()
2916        tcenters = tmesh.cell_centers
2917
2918        ids = surf.inside_points(tcenters, return_ids=True)
2919        ins = np.zeros(tmesh.ncells)
2920        ins[ids] = 1
2921
2922        if debug:
2923            # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close()
2924            edges = self.edges
2925            points = self.vertices
2926            elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :])
2927            histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d))
2928            print(".. edges min, max", elen.min(), elen.max())
2929            fillpts.cmap("bone")
2930            vedo.show(
2931                [
2932                    [
2933                        f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}",
2934                        surf.wireframe().alpha(0.2),
2935                        vedo.addons.Axes(surf),
2936                        fillpts,
2937                        Points(subpts).c("r4").ps(3),
2938                    ],
2939                    [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo],
2940                ],
2941                N=2,
2942                sharecam=False,
2943                new=True,
2944            ).close()
2945            print(".. thresholding")
2946
2947        tmesh.celldata["inside"] = ins.astype(np.uint8)
2948        tmesh.threshold("inside", above=0.9)
2949        tmesh.celldata.remove("inside")
2950
2951        if debug:
2952            print(f".. tetralize() completed, ntets = {tmesh.ncells}")
2953
2954        tmesh.pipeline = OperationNode(
2955            "tetralize",
2956            parents=[self],
2957            comment=f"#tets = {tmesh.ncells}",
2958            c="#e9c46a:#9e2a2b",
2959        )
2960        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
points
cell_centers
lines
lines_as_flat_array
mark_boundaries
find_cells_in_bounds
find_cells_along_line
find_cells_along_plane
keep_cell_types
map_cells_to_points
vertices
coordinates
cells_as_flat_array
cells
map_points_to_cells
resample_data_from
interpolate_data_from
add_ids
gradient
divergence
vorticity
probe
compute_cell_size
generate_random_data
integrate_data
write
tomesh
unsigned_distance
smooth_data
compute_streamlines