vedo.mesh

Submodule to work with polygonal meshes

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

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

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

Initialize a Mesh object.

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

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

DEPRECATED. Use property mesh.cells instead.

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

Return an array containing the edges connectivity.

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

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

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

Compute cell and vertex normals for the mesh.

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

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

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

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

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

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

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

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

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

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

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

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

Return True if the mesh is manifold.

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

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

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

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

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

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

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

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

Arguments:
  • field_id : (int) the id of the scalar field to use.
Example:
from vedo import *
mesh = Mesh("https://discourse.paraview.org/uploads/short-url/qVuZ1fiRjwhE1qYtgGE2HGXybgo.stl")
mesh.rotate_x(10).rotate_y(15).alpha(0.5)
mesh.pointdata["scalars"] = mesh.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:
561    def shrink(self, fraction=0.85) -> Self:
562        """
563        Shrink the triangle polydata in the representation of the input mesh.
564
565        Examples:
566            - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py)
567
568            ![](https://vedo.embl.es/images/basic/shrink.png)
569        """
570        # Overriding base class method core.shrink()
571        shrink = vtki.new("ShrinkPolyData")
572        shrink.SetInputData(self.dataset)
573        shrink.SetShrinkFactor(fraction)
574        shrink.Update()
575        self._update(shrink.GetOutput())
576        self.pipeline = OperationNode("shrink", parents=[self])
577        return self

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

Examples:

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

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

Examples:

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

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

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

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

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

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

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

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

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

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

Returns:

list of shapes.Lines

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

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

Join booundary lines by creating a triangle strip between them.

Example:

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

Split polylines into separate segments.

def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
848    def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
849        """
850        Slice a mesh with a plane and fill the contour.
851
852        Example:
853            ```python
854            from vedo import *
855            msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe()
856            mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0])
857            mslice.c('purple5')
858            show(msh, mslice, axes=1)
859            ```
860            ![](https://vedo.embl.es/images/feats/mesh_slice.jpg)
861
862        See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`.
863        """
864        intersection = self.intersect_with_plane(origin=origin, normal=normal)
865        slices = [s.triangulate() for s in intersection.join_segments()]
866        mslices = vedo.pointcloud.merge(slices)
867        if mslices:
868            mslices.name = "MeshSlice"
869            mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}")
870        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:
872    def triangulate(self, verts=True, lines=True) -> Self:
873        """
874        Converts mesh polygons into triangles.
875
876        If the input mesh is only made of 2D lines (no faces) the output will be a triangulation
877        that fills the internal area. The contours may be concave, and may even contain holes,
878        i.e. a contour may contain an internal contour winding in the opposite
879        direction to indicate that it is a hole.
880
881        Arguments:
882            verts : (bool)
883                if True, break input vertex cells into individual vertex cells (one point per cell).
884                If False, the input vertex cells will be ignored.
885            lines : (bool)
886                if True, break input polylines into line segments.
887                If False, input lines will be ignored and the output will have no lines.
888        """
889        if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips():
890            # print("Using vtkTriangleFilter")
891            tf = vtki.new("TriangleFilter")
892            tf.SetPassLines(lines)
893            tf.SetPassVerts(verts)
894
895        elif self.dataset.GetNumberOfLines():
896            # print("Using vtkContourTriangulator")
897            tf = vtki.new("ContourTriangulator")
898            tf.TriangulationErrorDisplayOn()
899
900        else:
901            vedo.logger.debug("input in triangulate() seems to be void! Skip.")
902            return self
903
904        tf.SetInputData(self.dataset)
905        tf.Update()
906        self._update(tf.GetOutput(), reset_locators=False)
907        self.lw(0).lighting("default").pickable()
908
909        self.pipeline = OperationNode(
910            "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}"
911        )
912        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:
914    def compute_cell_vertex_count(self) -> Self:
915        """
916        Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.
917        """
918        csf = vtki.new("CellSizeFilter")
919        csf.SetInputData(self.dataset)
920        csf.SetComputeArea(False)
921        csf.SetComputeVolume(False)
922        csf.SetComputeLength(False)
923        csf.SetComputeVertexCount(True)
924        csf.SetVertexCountArrayName("VertexCount")
925        csf.Update()
926        self.dataset.GetCellData().AddArray(
927            csf.GetOutput().GetCellData().GetArray("VertexCount")
928        )
929        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:
931    def compute_quality(self, metric=6) -> Self:
932        """
933        Calculate metrics of quality for the elements of a triangular mesh.
934        This method adds to the mesh a cell array named "Quality".
935        See class 
936        [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html).
937
938        Arguments:
939            metric : (int)
940                type of available estimators are:
941                - EDGE RATIO, 0
942                - ASPECT RATIO, 1
943                - RADIUS RATIO, 2
944                - ASPECT FROBENIUS, 3
945                - MED ASPECT FROBENIUS, 4
946                - MAX ASPECT FROBENIUS, 5
947                - MIN_ANGLE, 6
948                - COLLAPSE RATIO, 7
949                - MAX ANGLE, 8
950                - CONDITION, 9
951                - SCALED JACOBIAN, 10
952                - SHEAR, 11
953                - RELATIVE SIZE SQUARED, 12
954                - SHAPE, 13
955                - SHAPE AND SIZE, 14
956                - DISTORTION, 15
957                - MAX EDGE RATIO, 16
958                - SKEW, 17
959                - TAPER, 18
960                - VOLUME, 19
961                - STRETCH, 20
962                - DIAGONAL, 21
963                - DIMENSION, 22
964                - ODDY, 23
965                - SHEAR AND SIZE, 24
966                - JACOBIAN, 25
967                - WARPAGE, 26
968                - ASPECT GAMMA, 27
969                - AREA, 28
970                - ASPECT BETA, 29
971
972        Examples:
973            - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py)
974
975            ![](https://vedo.embl.es/images/advanced/meshquality.png)
976        """
977        qf = vtki.new("MeshQuality")
978        qf.SetInputData(self.dataset)
979        qf.SetTriangleQualityMeasure(metric)
980        qf.SaveCellQualityOn()
981        qf.Update()
982        self._update(qf.GetOutput(), reset_locators=False)
983        self.mapper.SetScalarModeToUseCellData()
984        self.pipeline = OperationNode("compute_quality", parents=[self])
985        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:
987    def count_vertices(self) -> np.ndarray:
988        """Count the number of vertices each cell has and return it as a numpy array"""
989        vc = vtki.new("CountVertices")
990        vc.SetInputData(self.dataset)
991        vc.SetOutputArrayName("VertexCount")
992        vc.Update()
993        varr = vc.GetOutput().GetCellData().GetArray("VertexCount")
994        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:
 996    def check_validity(self, tol=0) -> np.ndarray:
 997        """
 998        Return a numpy array of possible problematic faces following this convention:
 999        - Valid               =  0
1000        - WrongNumberOfPoints =  1
1001        - IntersectingEdges   =  2
1002        - IntersectingFaces   =  4
1003        - NoncontiguousEdges  =  8
1004        - Nonconvex           = 10
1005        - OrientedIncorrectly = 20
1006
1007        Arguments:
1008            tol : (float)
1009                value is used as an epsilon for floating point
1010                equality checks throughout the cell checking process.
1011        """
1012        vald = vtki.new("CellValidator")
1013        if tol:
1014            vald.SetTolerance(tol)
1015        vald.SetInputData(self.dataset)
1016        vald.Update()
1017        varr = vald.GetOutput().GetCellData().GetArray("ValidityState")
1018        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:
1020    def compute_curvature(self, method=0) -> Self:
1021        """
1022        Add scalars to `Mesh` that contains the curvature calculated in three different ways.
1023
1024        Variable `method` can be:
1025        - 0 = gaussian
1026        - 1 = mean curvature
1027        - 2 = max curvature
1028        - 3 = min curvature
1029
1030        Example:
1031            ```python
1032            from vedo import Torus
1033            Torus().compute_curvature().add_scalarbar().show().close()
1034            ```
1035            ![](https://vedo.embl.es/images/advanced/torus_curv.png)
1036        """
1037        curve = vtki.new("Curvatures")
1038        curve.SetInputData(self.dataset)
1039        curve.SetCurvatureType(method)
1040        curve.Update()
1041        self._update(curve.GetOutput(), reset_locators=False)
1042        self.mapper.ScalarVisibilityOn()
1043        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:
1045    def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Self:
1046        """
1047        Add to `Mesh` a scalar array that contains distance along a specified direction.
1048
1049        Arguments:
1050            low : (list)
1051                one end of the line (small scalar values)
1052            high : (list)
1053                other end of the line (large scalar values)
1054            vrange : (list)
1055                set the range of the scalar
1056
1057        Example:
1058            ```python
1059            from vedo import Sphere
1060            s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1))
1061            s.add_scalarbar().show(axes=1).close()
1062            ```
1063            ![](https://vedo.embl.es/images/basic/compute_elevation.png)
1064        """
1065        ef = vtki.new("ElevationFilter")
1066        ef.SetInputData(self.dataset)
1067        ef.SetLowPoint(low)
1068        ef.SetHighPoint(high)
1069        ef.SetScalarRange(vrange)
1070        ef.Update()
1071        self._update(ef.GetOutput(), reset_locators=False)
1072        self.mapper.ScalarVisibilityOn()
1073        return self

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

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

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

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

Arguments:
  • array_name : (str) name of the array to diffuse.
  • dt : (float) time step.
  • num_steps : (int) number of iterations.
def subdivide(self, n=1, method=0, mel=None) -> Self:
1148    def subdivide(self, n=1, method=0, mel=None) -> Self:
1149        """
1150        Increase the number of vertices of a surface mesh.
1151
1152        Arguments:
1153            n : (int)
1154                number of subdivisions.
1155            method : (int)
1156                Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
1157            mel : (float)
1158                Maximum Edge Length (applicable to Adaptive method only).
1159        """
1160        triangles = vtki.new("TriangleFilter")
1161        triangles.SetInputData(self.dataset)
1162        triangles.Update()
1163        tri_mesh = triangles.GetOutput()
1164        if method == 0:
1165            sdf = vtki.new("LoopSubdivisionFilter")
1166        elif method == 1:
1167            sdf = vtki.new("LinearSubdivisionFilter")
1168        elif method == 2:
1169            sdf = vtki.new("AdaptiveSubdivisionFilter")
1170            if mel is None:
1171                mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n
1172            sdf.SetMaximumEdgeLength(mel)
1173        elif method == 3:
1174            sdf = vtki.new("ButterflySubdivisionFilter")
1175        elif method == 4:
1176            sdf = vtki.new("DensifyPolyData")
1177        else:
1178            vedo.logger.error(f"in subdivide() unknown method {method}")
1179            raise RuntimeError()
1180
1181        if method != 2:
1182            sdf.SetNumberOfSubdivisions(n)
1183
1184        sdf.SetInputData(tri_mesh)
1185        sdf.Update()
1186
1187        self._update(sdf.GetOutput())
1188
1189        self.pipeline = OperationNode(
1190            "subdivide",
1191            parents=[self],
1192            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1193        )
1194        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:
1197    def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Self:
1198        """
1199        Downsample the number of vertices in a mesh to `fraction`.
1200
1201        This filter preserves the `pointdata` of the input dataset. In previous versions
1202        of vedo, this decimation algorithm was referred to as quadric decimation.
1203
1204        Arguments:
1205            fraction : (float)
1206                the desired target of reduction.
1207            n : (int)
1208                the desired number of final points
1209                (`fraction` is recalculated based on it).
1210            preserve_volume : (bool)
1211                Decide whether to activate volume preservation which greatly
1212                reduces errors in triangle normal direction.
1213            regularization : (float)
1214                regularize the point finding algorithm so as to have better quality
1215                mesh elements at the cost of a slightly lower precision on the
1216                geometry potentially (mostly at sharp edges).
1217                Can be useful for decimating meshes that have been triangulated on noisy data.
1218
1219        Note:
1220            Setting `fraction=0.1` leaves 10% of the original number of vertices.
1221            Internally the VTK class
1222            [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html)
1223            is used for this operation.
1224        
1225        See also: `decimate_binned()` and `decimate_pro()`.
1226        """
1227        poly = self.dataset
1228        if n:  # N = desired number of points
1229            npt = poly.GetNumberOfPoints()
1230            fraction = n / npt
1231            if fraction >= 1:
1232                return self
1233
1234        decimate = vtki.new("QuadricDecimation")
1235        decimate.SetVolumePreservation(preserve_volume)
1236        # decimate.AttributeErrorMetricOn()
1237        if regularization:
1238            decimate.SetRegularize(True)
1239            decimate.SetRegularization(regularization)
1240
1241        try:
1242            decimate.MapPointDataOn()
1243        except AttributeError:
1244            pass
1245
1246        decimate.SetTargetReduction(1 - fraction)
1247        decimate.SetInputData(poly)
1248        decimate.Update()
1249
1250        self._update(decimate.GetOutput())
1251        self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction()
1252
1253        self.pipeline = OperationNode(
1254            "decimate",
1255            parents=[self],
1256            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1257        )
1258        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:
1260    def decimate_pro(
1261            self,
1262            fraction=0.5,
1263            n=None,
1264            preserve_topology=True,
1265            preserve_boundaries=True,
1266            splitting=False,
1267            splitting_angle=75,
1268            feature_angle=0,
1269            inflection_point_ratio=10,
1270            vertex_degree=0,
1271        ) -> Self:
1272        """
1273        Downsample the number of vertices in a mesh to `fraction`.
1274
1275        This filter preserves the `pointdata` of the input dataset.
1276
1277        Arguments:
1278            fraction : (float)
1279                The desired target of reduction.
1280                Setting `fraction=0.1` leaves 10% of the original number of vertices.
1281            n : (int)
1282                the desired number of final points (`fraction` is recalculated based on it).
1283            preserve_topology : (bool)
1284                If on, mesh splitting and hole elimination will not occur.
1285                This may limit the maximum reduction that may be achieved.
1286            preserve_boundaries : (bool)
1287                Turn on/off the deletion of vertices on the boundary of a mesh.
1288                Control whether mesh boundaries are preserved during decimation.
1289            feature_angle : (float)
1290                Specify the angle that defines a feature.
1291                This angle is used to define what an edge is
1292                (i.e., if the surface normal between two adjacent triangles
1293                is >= FeatureAngle, an edge exists).
1294            splitting : (bool)
1295                Turn on/off the splitting of the mesh at corners,
1296                along edges, at non-manifold points, or anywhere else a split is required.
1297                Turning splitting off will better preserve the original topology of the mesh,
1298                but you may not obtain the requested reduction.
1299            splitting_angle : (float)
1300                Specify the angle that defines a sharp edge.
1301                This angle is used to control the splitting of the mesh.
1302                A split line exists when the surface normals between two edge connected triangles
1303                are >= `splitting_angle`.
1304            inflection_point_ratio : (float)
1305                An inflection point occurs when the ratio of reduction error between two iterations
1306                is greater than or equal to the `inflection_point_ratio` value.
1307            vertex_degree : (int)
1308                If the number of triangles connected to a vertex exceeds it then the vertex will be split.
1309
1310        Note:
1311            Setting `fraction=0.1` leaves 10% of the original number of vertices
1312        
1313        See also:
1314            `decimate()` and `decimate_binned()`.
1315        """
1316        poly = self.dataset
1317        if n:  # N = desired number of points
1318            npt = poly.GetNumberOfPoints()
1319            fraction = n / npt
1320            if fraction >= 1:
1321                return self
1322
1323        decimate = vtki.new("DecimatePro")
1324        decimate.SetPreserveTopology(preserve_topology)
1325        decimate.SetBoundaryVertexDeletion(preserve_boundaries)
1326        if feature_angle:
1327            decimate.SetFeatureAngle(feature_angle)
1328        decimate.SetSplitting(splitting)
1329        decimate.SetSplitAngle(splitting_angle)
1330        decimate.SetInflectionPointRatio(inflection_point_ratio)
1331        if vertex_degree:
1332            decimate.SetDegree(vertex_degree)
1333
1334        decimate.SetTargetReduction(1 - fraction)
1335        decimate.SetInputData(poly)
1336        decimate.Update()
1337        self._update(decimate.GetOutput())
1338
1339        self.pipeline = OperationNode(
1340            "decimate_pro",
1341            parents=[self],
1342            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1343        )
1344        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:
1346    def decimate_binned(self, divisions=(), use_clustering=False) -> Self:
1347        """
1348        Downsample the number of vertices in a mesh.
1349        
1350        This filter preserves the `celldata` of the input dataset,
1351        if `use_clustering=True` also the `pointdata` will be preserved in the result.
1352
1353        Arguments:
1354            divisions : (list)
1355                number of divisions along x, y and z axes.
1356            auto_adjust : (bool)
1357                if True, the number of divisions is automatically adjusted to
1358                create more uniform cells.
1359            use_clustering : (bool)
1360                use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html)
1361                instead of 
1362                [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html).
1363        
1364        See also: `decimate()` and `decimate_pro()`.
1365        """
1366        if use_clustering:
1367            decimate = vtki.new("QuadricClustering")
1368            decimate.CopyCellDataOn()
1369        else:
1370            decimate = vtki.new("BinnedDecimation")
1371            decimate.ProducePointDataOn()
1372            decimate.ProduceCellDataOn()
1373
1374        decimate.SetInputData(self.dataset)
1375
1376        if len(divisions) == 0:
1377            decimate.SetAutoAdjustNumberOfDivisions(1)
1378        else:
1379            decimate.SetAutoAdjustNumberOfDivisions(0)
1380            decimate.SetNumberOfDivisions(divisions)
1381        decimate.Update()
1382
1383        self._update(decimate.GetOutput())
1384        self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions()
1385        self.pipeline = OperationNode(
1386            "decimate_binned",
1387            parents=[self],
1388            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1389        )
1390        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:
1392    def generate_random_points(self, n: int, min_radius=0.0) -> "Points":
1393        """
1394        Generate `n` uniformly distributed random points
1395        inside the polygonal mesh.
1396
1397        A new point data array is added to the output points
1398        called "OriginalCellID" which contains the index of
1399        the cell ID in which the point was generated.
1400
1401        Arguments:
1402            n : (int)
1403                number of points to generate.
1404            min_radius: (float)
1405                impose a minimum distance between points.
1406                If `min_radius` is set to 0, the points are
1407                generated uniformly at random inside the mesh.
1408                If `min_radius` is set to a positive value,
1409                the points are generated uniformly at random
1410                inside the mesh, but points closer than `min_radius`
1411                to any other point are discarded.
1412
1413        Returns a `vedo.Points` object.
1414
1415        Note:
1416            Consider using `points.probe(msh)` or
1417            `points.interpolate_data_from(msh)`
1418            to interpolate existing mesh data onto the new points.
1419
1420        Example:
1421        ```python
1422        from vedo import *
1423        msh = Mesh(dataurl + "panther.stl").lw(2)
1424        pts = msh.generate_random_points(20000, min_radius=0.5)
1425        print("Original cell ids:", pts.pointdata["OriginalCellID"])
1426        show(pts, msh, axes=1).close()
1427        ```
1428        """
1429        cmesh = self.clone().clean().triangulate().compute_cell_size()
1430        triangles = cmesh.cells
1431        vertices = cmesh.vertices
1432        cumul = np.cumsum(cmesh.celldata["Area"])
1433
1434        out_pts = []
1435        orig_cell = []
1436        for _ in range(n):
1437            # choose a triangle based on area
1438            random_area = np.random.random() * cumul[-1]
1439            it = np.searchsorted(cumul, random_area)
1440            A, B, C = vertices[triangles[it]]
1441            # calculate the random point in the triangle
1442            r1, r2 = np.random.random(2)
1443            if r1 + r2 > 1:
1444                r1 = 1 - r1
1445                r2 = 1 - r2
1446            out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C)
1447            orig_cell.append(it)
1448        nporig_cell = np.array(orig_cell, dtype=np.uint32)
1449
1450        vpts = Points(out_pts)
1451        vpts.pointdata["OriginalCellID"] = nporig_cell
1452
1453        if min_radius > 0:
1454            vpts.subsample(min_radius, absolute=True)
1455
1456        vpts.point_size(5).color("k1")
1457        vpts.name = "RandomPoints"
1458        vpts.pipeline = OperationNode(
1459            "generate_random_points", c="#edabab", parents=[self])
1460        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:
1462    def delete_cells(self, ids: List[int]) -> Self:
1463        """
1464        Remove cells from the mesh object by their ID.
1465        Points (vertices) are not removed (you may use `clean()` to remove those).
1466        """
1467        self.dataset.BuildLinks()
1468        for cid in ids:
1469            self.dataset.DeleteCell(cid)
1470        self.dataset.RemoveDeletedCells()
1471        self.dataset.Modified()
1472        self.mapper.Modified()
1473        self.pipeline = OperationNode(
1474            "delete_cells",
1475            parents=[self],
1476            comment=f"#cells {self.dataset.GetNumberOfCells()}",
1477        )
1478        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:
1480    def delete_cells_by_point_index(self, indices: List[int]) -> Self:
1481        """
1482        Delete a list of vertices identified by any of their vertex index.
1483
1484        See also `delete_cells()`.
1485
1486        Examples:
1487            - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py)
1488
1489                ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png)
1490        """
1491        cell_ids = vtki.vtkIdList()
1492        self.dataset.BuildLinks()
1493        n = 0
1494        for i in np.unique(indices):
1495            self.dataset.GetPointCells(i, cell_ids)
1496            for j in range(cell_ids.GetNumberOfIds()):
1497                self.dataset.DeleteCell(cell_ids.GetId(j))  # flag cell
1498                n += 1
1499
1500        self.dataset.RemoveDeletedCells()
1501        self.dataset.Modified()
1502        self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self])
1503        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:
1505    def collapse_edges(self, distance: float, iterations=1) -> Self:
1506        """
1507        Collapse mesh edges so that are all above `distance`.
1508        
1509        Example:
1510            ```python
1511            from vedo import *
1512            np.random.seed(2)
1513            grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
1514            grid1.celldata['scalar'] = grid1.cell_centers[:,1]
1515            grid2 = grid1.clone().collapse_edges(0.1)
1516            show(grid1, grid2, N=2, axes=1)
1517            ```
1518        """
1519        for _ in range(iterations):
1520            medges = self.edges
1521            pts = self.vertices
1522            newpts = np.array(pts)
1523            moved = []
1524            for e in medges:
1525                if len(e) == 2:
1526                    id0, id1 = e
1527                    p0, p1 = pts[id0], pts[id1]
1528                    if (np.linalg.norm(p1-p0) < distance 
1529                        and id0 not in moved
1530                        and id1 not in moved
1531                    ):
1532                        p = (p0 + p1) / 2
1533                        newpts[id0] = p
1534                        newpts[id1] = p
1535                        moved += [id0, id1]
1536            self.vertices = newpts
1537            cpd = vtki.new("CleanPolyData")
1538            cpd.ConvertLinesToPointsOff()
1539            cpd.ConvertPolysToLinesOff()
1540            cpd.ConvertStripsToPolysOff()
1541            cpd.SetInputData(self.dataset)
1542            cpd.Update()
1543            self._update(cpd.GetOutput())
1544
1545        self.pipeline = OperationNode(
1546            "collapse_edges",
1547            parents=[self],
1548            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1549        )
1550        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]:
1552    def adjacency_list(self) -> List[set]:
1553        """
1554        Computes the adjacency list for mesh edge-graph.
1555
1556        Returns: 
1557            a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex
1558        """
1559        inc = [set()] * self.nvertices
1560        for cell in self.cells:
1561            nc = len(cell)
1562            if nc > 1:
1563                for i in range(nc-1):
1564                    ci = cell[i]
1565                    inc[ci] = inc[ci].union({cell[i-1], cell[i+1]})
1566        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:
1568    def graph_ball(self, index, n: int) -> set:
1569        """
1570        Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`.
1571
1572        Arguments:
1573            index : (int)
1574                index of the vertex
1575            n : (int)
1576                radius in the graph metric
1577
1578        Returns:
1579            the set of indices of the vertices which are at most `n` edges from vertex `index`.
1580        """
1581        if n == 0:
1582            return {index}
1583        else:
1584            al = self.adjacency_list()
1585            ball = {index}
1586            i = 0
1587            while i < n and len(ball) < self.nvertices:
1588                for v in ball:
1589                    ball = ball.union(al[v])
1590                i += 1
1591            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:
1593    def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Self:
1594        """
1595        Adjust mesh point positions using the so-called "Windowed Sinc" method.
1596
1597        Arguments:
1598            niter : (int)
1599                number of iterations.
1600            pass_band : (float)
1601                set the pass_band value for the windowed sinc filter.
1602            edge_angle : (float)
1603                edge angle to control smoothing along edges (either interior or boundary).
1604            feature_angle : (float)
1605                specifies the feature angle for sharp edge identification.
1606            boundary : (bool)
1607                specify if boundary should also be smoothed or kept unmodified
1608
1609        Examples:
1610            - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py)
1611
1612            ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png)
1613        """
1614        cl = vtki.new("CleanPolyData")
1615        cl.SetInputData(self.dataset)
1616        cl.Update()
1617        smf = vtki.new("WindowedSincPolyDataFilter")
1618        smf.SetInputData(cl.GetOutput())
1619        smf.SetNumberOfIterations(niter)
1620        smf.SetEdgeAngle(edge_angle)
1621        smf.SetFeatureAngle(feature_angle)
1622        smf.SetPassBand(pass_band)
1623        smf.NormalizeCoordinatesOn()
1624        smf.NonManifoldSmoothingOn()
1625        smf.FeatureEdgeSmoothingOn()
1626        smf.SetBoundarySmoothing(boundary)
1627        smf.Update()
1628
1629        self._update(smf.GetOutput())
1630
1631        self.pipeline = OperationNode(
1632            "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1633        )
1634        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:
1636    def fill_holes(self, size=None) -> Self:
1637        """
1638        Identifies and fills holes in the input mesh.
1639        Holes are identified by locating boundary edges, linking them together
1640        into loops, and then triangulating the resulting loops.
1641
1642        Arguments:
1643            size : (float)
1644                Approximate limit to the size of the hole that can be filled.
1645
1646        Examples:
1647            - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py)
1648        """
1649        fh = vtki.new("FillHolesFilter")
1650        if not size:
1651            mb = self.diagonal_size()
1652            size = mb / 10
1653        fh.SetHoleSize(size)
1654        fh.SetInputData(self.dataset)
1655        fh.Update()
1656
1657        self._update(fh.GetOutput())
1658
1659        self.pipeline = OperationNode(
1660            "fill_holes",
1661            parents=[self],
1662            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1663        )
1664        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:
1666    def contains(self, point: tuple, tol=1e-05) -> bool:
1667        """
1668        Return True if point is inside a polydata closed surface.
1669        
1670        Note:
1671            if you have many points to check use `inside_points()` instead.
1672        
1673        Example:
1674            ```python
1675            from vedo import *
1676            s = Sphere().c('green5').alpha(0.5)
1677            pt  = [0.1, 0.2, 0.3]
1678            print("Sphere contains", pt, s.contains(pt))
1679            show(s, Point(pt), axes=1).close()
1680            ```      
1681        """
1682        points = vtki.vtkPoints()
1683        points.InsertNextPoint(point)
1684        poly = vtki.vtkPolyData()
1685        poly.SetPoints(points)
1686        sep = vtki.new("SelectEnclosedPoints")
1687        sep.SetTolerance(tol)
1688        sep.CheckSurfaceOff()
1689        sep.SetInputData(poly)
1690        sep.SetSurfaceData(self.dataset)
1691        sep.Update()
1692        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]:
1694    def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]:
1695        """
1696        Return the point cloud that is inside mesh surface as a new Points object.
1697
1698        If return_ids is True a list of IDs is returned and in addition input points
1699        are marked by a pointdata array named "IsInside".
1700
1701        Example:
1702            `print(pts.pointdata["IsInside"])`
1703
1704        Examples:
1705            - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
1706
1707            ![](https://vedo.embl.es/images/basic/pca.png)
1708        """
1709        if isinstance(pts, Points):
1710            poly = pts.dataset
1711            ptsa = pts.vertices
1712        else:
1713            ptsa = np.asarray(pts)
1714            vpoints = vtki.vtkPoints()
1715            vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32))
1716            poly = vtki.vtkPolyData()
1717            poly.SetPoints(vpoints)
1718
1719        sep = vtki.new("SelectEnclosedPoints")
1720        # sep = vtki.new("ExtractEnclosedPoints()
1721        sep.SetTolerance(tol)
1722        sep.SetInputData(poly)
1723        sep.SetSurfaceData(self.dataset)
1724        sep.SetInsideOut(invert)
1725        sep.Update()
1726
1727        varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints")
1728        mask = vtk2numpy(varr).astype(bool)
1729        ids = np.array(range(len(ptsa)), dtype=int)[mask]
1730
1731        if isinstance(pts, Points):
1732            varr.SetName("IsInside")
1733            pts.dataset.GetPointData().AddArray(varr)
1734
1735        if return_ids:
1736            return ids
1737
1738        pcl = Points(ptsa[ids])
1739        pcl.name = "InsidePoints"
1740
1741        pcl.pipeline = OperationNode(
1742            "inside_points",
1743            parents=[self, ptsa],
1744            comment=f"#pts {pcl.dataset.GetNumberOfPoints()}",
1745        )
1746        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]:
1748    def boundaries(
1749        self,
1750        boundary_edges=True,
1751        manifold_edges=False,
1752        non_manifold_edges=False,
1753        feature_angle=None,
1754        return_point_ids=False,
1755        return_cell_ids=False,
1756        cell_edge=False,
1757    ) -> Union[Self, np.ndarray]:
1758        """
1759        Return the boundary lines of an input mesh.
1760        Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method.
1761
1762        Arguments:
1763            boundary_edges : (bool)
1764                Turn on/off the extraction of boundary edges.
1765            manifold_edges : (bool)
1766                Turn on/off the extraction of manifold edges.
1767            non_manifold_edges : (bool)
1768                Turn on/off the extraction of non-manifold edges.
1769            feature_angle : (bool)
1770                Specify the min angle btw 2 faces for extracting edges.
1771            return_point_ids : (bool)
1772                return a numpy array of point indices
1773            return_cell_ids : (bool)
1774                return a numpy array of cell indices
1775            cell_edge : (bool)
1776                set to `True` if a cell need to share an edge with
1777                the boundary line, or `False` if a single vertex is enough
1778
1779        Examples:
1780            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)
1781
1782            ![](https://vedo.embl.es/images/basic/boundaries.png)
1783        """
1784        fe = vtki.new("FeatureEdges")
1785        fe.SetBoundaryEdges(boundary_edges)
1786        fe.SetNonManifoldEdges(non_manifold_edges)
1787        fe.SetManifoldEdges(manifold_edges)
1788        try:
1789            fe.SetPassLines(True) # vtk9.2
1790        except AttributeError:
1791            pass
1792        fe.ColoringOff()
1793        fe.SetFeatureEdges(False)
1794        if feature_angle is not None:
1795            fe.SetFeatureEdges(True)
1796            fe.SetFeatureAngle(feature_angle)
1797
1798        if return_point_ids or return_cell_ids:
1799            idf = vtki.new("IdFilter")
1800            idf.SetInputData(self.dataset)
1801            idf.SetPointIdsArrayName("BoundaryIds")
1802            idf.SetPointIds(True)
1803            idf.Update()
1804
1805            fe.SetInputData(idf.GetOutput())
1806            fe.Update()
1807
1808            vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds")
1809            npid = vtk2numpy(vid).astype(int)
1810
1811            if return_point_ids:
1812                return npid
1813
1814            if return_cell_ids:
1815                n = 1 if cell_edge else 0
1816                inface = []
1817                for i, face in enumerate(self.cells):
1818                    # isin = np.any([vtx in npid for vtx in face])
1819                    isin = 0
1820                    for vtx in face:
1821                        isin += int(vtx in npid)
1822                        if isin > n:
1823                            break
1824                    if isin > n:
1825                        inface.append(i)
1826                return np.array(inface).astype(int)
1827
1828            return self
1829
1830        else:
1831
1832            fe.SetInputData(self.dataset)
1833            fe.Update()
1834            msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off")
1835            msh.name = "MeshBoundaries"
1836
1837            msh.pipeline = OperationNode(
1838                "boundaries",
1839                parents=[self],
1840                shape="octagon",
1841                comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
1842            )
1843            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:
1845    def imprint(self, loopline, tol=0.01) -> Self:
1846        """
1847        Imprint the contact surface of one object onto another surface.
1848
1849        Arguments:
1850            loopline : (vedo.Line)
1851                a Line object to be imprinted onto the mesh.
1852            tol : (float)
1853                projection tolerance which controls how close the imprint
1854                surface must be to the target.
1855
1856        Example:
1857            ```python
1858            from vedo import *
1859            grid = Grid()#.triangulate()
1860            circle = Circle(r=0.3, res=24).pos(0.11,0.12)
1861            line = Line(circle, closed=True, lw=4, c='r4')
1862            grid.imprint(line)
1863            show(grid, line, axes=1).close()
1864            ```
1865            ![](https://vedo.embl.es/images/feats/imprint.png)
1866        """
1867        loop = vtki.new("ContourLoopExtraction")
1868        loop.SetInputData(loopline.dataset)
1869        loop.Update()
1870
1871        clean_loop = vtki.new("CleanPolyData")
1872        clean_loop.SetInputData(loop.GetOutput())
1873        clean_loop.Update()
1874
1875        imp = vtki.new("ImprintFilter")
1876        imp.SetTargetData(self.dataset)
1877        imp.SetImprintData(clean_loop.GetOutput())
1878        imp.SetTolerance(tol)
1879        imp.BoundaryEdgeInsertionOn()
1880        imp.TriangulateOutputOn()
1881        imp.Update()
1882
1883        self._update(imp.GetOutput())
1884
1885        self.pipeline = OperationNode(
1886            "imprint",
1887            parents=[self],
1888            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1889        )
1890        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]:
1892    def connected_vertices(self, index: int) -> List[int]:
1893        """Find all vertices connected to an input vertex specified by its index.
1894
1895        Examples:
1896            - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py)
1897
1898            ![](https://vedo.embl.es/images/basic/connVtx.png)
1899        """
1900        poly = self.dataset
1901
1902        cell_idlist = vtki.vtkIdList()
1903        poly.GetPointCells(index, cell_idlist)
1904
1905        idxs = []
1906        for i in range(cell_idlist.GetNumberOfIds()):
1907            point_idlist = vtki.vtkIdList()
1908            poly.GetCellPoints(cell_idlist.GetId(i), point_idlist)
1909            for j in range(point_idlist.GetNumberOfIds()):
1910                idj = point_idlist.GetId(j)
1911                if idj == index:
1912                    continue
1913                if idj in idxs:
1914                    continue
1915                idxs.append(idj)
1916
1917        return idxs

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

Examples:

def extract_cells(self, ids: List[int]) -> Self:
1919    def extract_cells(self, ids: List[int]) -> Self:
1920        """
1921        Extract a subset of cells from a mesh and return it as a new mesh.
1922        """
1923        selectCells = vtki.new("SelectionNode")
1924        selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL)
1925        selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES)
1926        idarr = vtki.vtkIdTypeArray()
1927        idarr.SetNumberOfComponents(1)
1928        idarr.SetNumberOfValues(len(ids))
1929        for i, v in enumerate(ids):
1930            idarr.SetValue(i, v)
1931        selectCells.SetSelectionList(idarr)
1932
1933        selection = vtki.new("Selection")
1934        selection.AddNode(selectCells)
1935
1936        extractSelection = vtki.new("ExtractSelection")
1937        extractSelection.SetInputData(0, self.dataset)
1938        extractSelection.SetInputData(1, selection)
1939        extractSelection.Update()
1940
1941        gf = vtki.new("GeometryFilter")
1942        gf.SetInputData(extractSelection.GetOutput())
1943        gf.Update()
1944        msh = Mesh(gf.GetOutput())
1945        msh.copy_properties_from(self)
1946        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]]:
1948    def connected_cells(self, index: int, return_ids=False) -> Union[Self, List[int]]:
1949        """Find all cellls connected to an input vertex specified by its index."""
1950
1951        # Find all cells connected to point index
1952        dpoly = self.dataset
1953        idlist = vtki.vtkIdList()
1954        dpoly.GetPointCells(index, idlist)
1955
1956        ids = vtki.vtkIdTypeArray()
1957        ids.SetNumberOfComponents(1)
1958        rids = []
1959        for k in range(idlist.GetNumberOfIds()):
1960            cid = idlist.GetId(k)
1961            ids.InsertNextValue(cid)
1962            rids.append(int(cid))
1963        if return_ids:
1964            return rids
1965
1966        selection_node = vtki.new("SelectionNode")
1967        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
1968        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
1969        selection_node.SetSelectionList(ids)
1970        selection = vtki.new("Selection")
1971        selection.AddNode(selection_node)
1972        extractSelection = vtki.new("ExtractSelection")
1973        extractSelection.SetInputData(0, dpoly)
1974        extractSelection.SetInputData(1, selection)
1975        extractSelection.Update()
1976        gf = vtki.new("GeometryFilter")
1977        gf.SetInputData(extractSelection.GetOutput())
1978        gf.Update()
1979        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:
1981    def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> Self:
1982        """
1983        Return a new line `Mesh` which corresponds to the outer `silhouette`
1984        of the input as seen along a specified `direction`, this can also be
1985        a `vtkCamera` object.
1986
1987        Arguments:
1988            direction : (list)
1989                viewpoint direction vector.
1990                If `None` this is guessed by looking at the minimum
1991                of the sides of the bounding box.
1992            border_edges : (bool)
1993                enable or disable generation of border edges
1994            feature_angle : (float)
1995                minimal angle for sharp edges detection.
1996                If set to `False` the functionality is disabled.
1997
1998        Examples:
1999            - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py)
2000
2001            ![](https://vedo.embl.es/images/basic/silhouette1.png)
2002        """
2003        sil = vtki.new("PolyDataSilhouette")
2004        sil.SetInputData(self.dataset)
2005        sil.SetBorderEdges(border_edges)
2006        if feature_angle is False:
2007            sil.SetEnableFeatureAngle(0)
2008        else:
2009            sil.SetEnableFeatureAngle(1)
2010            sil.SetFeatureAngle(feature_angle)
2011
2012        if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera:
2013            sil.SetCamera(vedo.plotter_instance.camera)
2014            m = Mesh()
2015            m.mapper.SetInputConnection(sil.GetOutputPort())
2016
2017        elif isinstance(direction, vtki.vtkCamera):
2018            sil.SetCamera(direction)
2019            m = Mesh()
2020            m.mapper.SetInputConnection(sil.GetOutputPort())
2021
2022        elif direction == "2d":
2023            sil.SetVector(3.4, 4.5, 5.6)  # random
2024            sil.SetDirectionToSpecifiedVector()
2025            sil.Update()
2026            m = Mesh(sil.GetOutput())
2027
2028        elif is_sequence(direction):
2029            sil.SetVector(direction)
2030            sil.SetDirectionToSpecifiedVector()
2031            sil.Update()
2032            m = Mesh(sil.GetOutput())
2033        else:
2034            vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}")
2035            vedo.logger.error("first render the scene with show() or specify camera/direction")
2036            return self
2037
2038        m.lw(2).c((0, 0, 0)).lighting("off")
2039        m.mapper.SetResolveCoincidentTopologyToPolygonOffset()
2040        m.pipeline = OperationNode("silhouette", parents=[self])
2041        m.name = "Silhouette"
2042        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:
2044    def isobands(self, n=10, vmin=None, vmax=None) -> Self:
2045        """
2046        Return a new `Mesh` representing the isobands of the active scalars.
2047        This is a new mesh where the scalar is now associated to cell faces and
2048        used to colorize the mesh.
2049
2050        Arguments:
2051            n : (int)
2052                number of isobands in the range
2053            vmin : (float)
2054                minimum of the range
2055            vmax : (float)
2056                maximum of the range
2057
2058        Examples:
2059            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
2060        """
2061        r0, r1 = self.dataset.GetScalarRange()
2062        if vmin is None:
2063            vmin = r0
2064        if vmax is None:
2065            vmax = r1
2066
2067        # --------------------------------
2068        bands = []
2069        dx = (vmax - vmin) / float(n)
2070        b = [vmin, vmin + dx / 2.0, vmin + dx]
2071        i = 0
2072        while i < n:
2073            bands.append(b)
2074            b = [b[0] + dx, b[1] + dx, b[2] + dx]
2075            i += 1
2076
2077        # annotate, use the midpoint of the band as the label
2078        lut = self.mapper.GetLookupTable()
2079        labels = []
2080        for b in bands:
2081            labels.append("{:4.2f}".format(b[1]))
2082        values = vtki.vtkVariantArray()
2083        for la in labels:
2084            values.InsertNextValue(vtki.vtkVariant(la))
2085        for i in range(values.GetNumberOfTuples()):
2086            lut.SetAnnotation(i, values.GetValue(i).ToString())
2087
2088        bcf = vtki.new("BandedPolyDataContourFilter")
2089        bcf.SetInputData(self.dataset)
2090        # Use either the minimum or maximum value for each band.
2091        for i, band in enumerate(bands):
2092            bcf.SetValue(i, band[2])
2093        # We will use an indexed lookup table.
2094        bcf.SetScalarModeToIndex()
2095        bcf.GenerateContourEdgesOff()
2096        bcf.Update()
2097        bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands")
2098
2099        m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True)
2100        m1.mapper.SetLookupTable(lut)
2101        m1.mapper.SetScalarRange(lut.GetRange())
2102        m1.pipeline = OperationNode("isobands", parents=[self])
2103        m1.name = "IsoBands"
2104        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:
2106    def isolines(self, n=10, vmin=None, vmax=None) -> Self:
2107        """
2108        Return a new `Mesh` representing the isolines of the active scalars.
2109
2110        Arguments:
2111            n : (int)
2112                number of isolines in the range
2113            vmin : (float)
2114                minimum of the range
2115            vmax : (float)
2116                maximum of the range
2117
2118        Examples:
2119            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
2120
2121            ![](https://vedo.embl.es/images/pyplot/isolines.png)
2122        """
2123        bcf = vtki.new("ContourFilter")
2124        bcf.SetInputData(self.dataset)
2125        r0, r1 = self.dataset.GetScalarRange()
2126        if vmin is None:
2127            vmin = r0
2128        if vmax is None:
2129            vmax = r1
2130        bcf.GenerateValues(n, vmin, vmax)
2131        bcf.Update()
2132        sf = vtki.new("Stripper")
2133        sf.SetJoinContiguousSegments(True)
2134        sf.SetInputData(bcf.GetOutput())
2135        sf.Update()
2136        cl = vtki.new("CleanPolyData")
2137        cl.SetInputData(sf.GetOutput())
2138        cl.Update()
2139        msh = Mesh(cl.GetOutput(), c="k").lighting("off")
2140        msh.mapper.SetResolveCoincidentTopologyToPolygonOffset()
2141        msh.pipeline = OperationNode("isolines", parents=[self])
2142        msh.name = "IsoLines"
2143        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:
2145    def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Self:
2146        """
2147        Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices.
2148        The input dataset is swept around the z-axis to create new polygonal primitives.
2149        For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.
2150
2151        You can control whether the sweep of a 2D object (i.e., polygon or triangle strip)
2152        is capped with the generating geometry.
2153        Also, you can control the angle of rotation, and whether translation along the z-axis
2154        is performed along with the rotation. (Translation is useful for creating "springs").
2155        You also can adjust the radius of the generating geometry using the "dR" keyword.
2156
2157        The skirt is generated by locating certain topological features.
2158        Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips)
2159        generate surfaces. This is true also of lines or polylines. Vertices generate lines.
2160
2161        This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses;
2162        or translational/rotational symmetric objects like springs or corkscrews.
2163
2164        Arguments:
2165            zshift : (float)
2166                shift along z axis.
2167            direction : (list)
2168                extrusion direction in the xy plane. 
2169                note that zshift is forced to be the 3rd component of direction,
2170                which is therefore ignored.
2171            rotation : (float)
2172                set the angle of rotation.
2173            dr : (float)
2174                set the radius variation in absolute units.
2175            cap : (bool)
2176                enable or disable capping.
2177            res : (int)
2178                set the resolution of the generating geometry.
2179
2180        Warning:
2181            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result
2182            in two separate surfaces if capping is on, or no surface if capping is off.
2183
2184        Examples:
2185            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2186
2187            ![](https://vedo.embl.es/images/basic/extrude.png)
2188        """
2189        rf = vtki.new("RotationalExtrusionFilter")
2190        # rf = vtki.new("LinearExtrusionFilter")
2191        rf.SetInputData(self.dataset)  # must not be transformed
2192        rf.SetResolution(res)
2193        rf.SetCapping(cap)
2194        rf.SetAngle(rotation)
2195        rf.SetTranslation(zshift)
2196        rf.SetDeltaRadius(dr)
2197        rf.Update()
2198
2199        # convert triangle strips to polygonal data
2200        tris = vtki.new("TriangleFilter")
2201        tris.SetInputData(rf.GetOutput())
2202        tris.Update()
2203
2204        m = Mesh(tris.GetOutput())
2205
2206        if len(direction) > 1:
2207            p = self.pos()
2208            LT = vedo.LinearTransform()
2209            LT.translate(-p)
2210            LT.concatenate([
2211                [1, 0, direction[0]],
2212                [0, 1, direction[1]],
2213                [0, 0, 1]
2214            ])
2215            LT.translate(p)
2216            m.apply_transform(LT)
2217
2218        m.copy_properties_from(self).flat().lighting("default")
2219        m.pipeline = OperationNode(
2220            "extrude", parents=[self], 
2221            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2222        )
2223        m.name = "ExtrudedMesh"
2224        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:
2226    def extrude_and_trim_with(
2227            self,
2228            surface: "Mesh",
2229            direction=(),
2230            strategy="all",
2231            cap=True,
2232            cap_strategy="max",
2233    ) -> Self:
2234        """
2235        Extrude a Mesh and trim it with an input surface mesh.
2236
2237        Arguments:
2238            surface : (Mesh)
2239                the surface mesh to trim with.
2240            direction : (list)
2241                extrusion direction in the xy plane.
2242            strategy : (str)
2243                either "boundary_edges" or "all_edges".
2244            cap : (bool)
2245                enable or disable capping.
2246            cap_strategy : (str)
2247                either "intersection", "minimum_distance", "maximum_distance", "average_distance".
2248
2249        The input Mesh is swept along a specified direction forming a "skirt"
2250        from the boundary edges 2D primitives (i.e., edges used by only one polygon);
2251        and/or from vertices and lines.
2252        The extent of the sweeping is limited by a second input: defined where
2253        the sweep intersects a user-specified surface.
2254
2255        Capping of the extrusion can be enabled.
2256        In this case the input, generating primitive is copied inplace as well
2257        as to the end of the extrusion skirt.
2258        (See warnings below on what happens if the intersecting sweep does not
2259        intersect, or partially intersects the trim surface.)
2260
2261        Note that this method operates in two fundamentally different modes
2262        based on the extrusion strategy. 
2263        If the strategy is "boundary_edges", then only the boundary edges of the input's
2264        2D primitives are extruded (verts and lines are extruded to generate lines and quads).
2265        However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives
2266        is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).
2267
2268        Warning:
2269            The extrusion direction is assumed to define an infinite line.
2270            The intersection with the trim surface is along a ray from the - to + direction,
2271            however only the first intersection is taken.
2272            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate
2273            surfaces if capping is on and "boundary_edges" enabled,
2274            or no surface if capping is off and "boundary_edges" is enabled.
2275            If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface,
2276            then no output for that primitive will be generated. In extreme cases, it is possible that no output
2277            whatsoever will be generated.
2278        
2279        Example:
2280            ```python
2281            from vedo import *
2282            sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
2283            circle = Circle([0,0,0], r=2, res=100).color('b6')
2284            extruded_circle = circle.extrude_and_trim_with(
2285                sphere, 
2286                direction=[0,-0.2,1],
2287                strategy="bound",
2288                cap=True,
2289                cap_strategy="intersection",
2290            )
2291            circle.lw(3).color("tomato").shift(dz=-0.1)
2292            show(circle, sphere, extruded_circle, axes=1).close()
2293            ```
2294        """
2295        trimmer = vtki.new("TrimmedExtrusionFilter")
2296        trimmer.SetInputData(self.dataset)
2297        trimmer.SetCapping(cap)
2298        trimmer.SetExtrusionDirection(direction)
2299        trimmer.SetTrimSurfaceData(surface.dataset)
2300        if "bound" in strategy:
2301            trimmer.SetExtrusionStrategyToBoundaryEdges()
2302        elif "all" in strategy:
2303            trimmer.SetExtrusionStrategyToAllEdges()
2304        else:
2305            vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}")
2306        # print (trimmer.GetExtrusionStrategy())
2307        
2308        if "intersect" in cap_strategy:
2309            trimmer.SetCappingStrategyToIntersection()
2310        elif "min" in cap_strategy:
2311            trimmer.SetCappingStrategyToMinimumDistance()
2312        elif "max" in cap_strategy:
2313            trimmer.SetCappingStrategyToMaximumDistance()
2314        elif "ave" in cap_strategy:
2315            trimmer.SetCappingStrategyToAverageDistance()
2316        else:
2317            vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}")
2318        # print (trimmer.GetCappingStrategy())
2319
2320        trimmer.Update()
2321
2322        m = Mesh(trimmer.GetOutput())
2323        m.copy_properties_from(self).flat().lighting("default")
2324        m.pipeline = OperationNode(
2325            "extrude_and_trim", parents=[self, surface],
2326            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2327        )
2328        m.name = "ExtrudedAndTrimmedMesh"
2329        return m

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

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

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

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

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

Warning:

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

Example:
from vedo import *
sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
circle = Circle([0,0,0], r=2, res=100).color('b6')
extruded_circle = circle.extrude_and_trim_with(
    sphere, 
    direction=[0,-0.2,1],
    strategy="bound",
    cap=True,
    cap_strategy="intersection",
)
circle.lw(3).color("tomato").shift(dz=-0.1)
show(circle, sphere, extruded_circle, axes=1).close()
def split( self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True) -> List[Self]:
2331    def split(
2332        self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True
2333    ) -> List[Self]:
2334        """
2335        Split a mesh by connectivity and order the pieces by increasing area.
2336
2337        Arguments:
2338            maxdepth : (int)
2339                only consider this maximum number of mesh parts.
2340            flag : (bool)
2341                if set to True return the same single object,
2342                but add a "RegionId" array to flag the mesh subparts
2343            must_share_edge : (bool)
2344                if True, mesh regions that only share single points will be split.
2345            sort_by_area : (bool)
2346                if True, sort the mesh parts by decreasing area.
2347
2348        Examples:
2349            - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py)
2350
2351            ![](https://vedo.embl.es/images/advanced/splitmesh.png)
2352        """
2353        pd = self.dataset
2354        if must_share_edge:
2355            if pd.GetNumberOfPolys() == 0:
2356                vedo.logger.warning("in split(): no polygons found. Skip.")
2357                return [self]
2358            cf = vtki.new("PolyDataEdgeConnectivityFilter")
2359            cf.BarrierEdgesOff()
2360        else:
2361            cf = vtki.new("PolyDataConnectivityFilter")
2362
2363        cf.SetInputData(pd)
2364        cf.SetExtractionModeToAllRegions()
2365        cf.SetColorRegions(True)
2366        cf.Update()
2367        out = cf.GetOutput()
2368
2369        if not out.GetNumberOfPoints():
2370            return [self]
2371
2372        if flag:
2373            self.pipeline = OperationNode("split mesh", parents=[self])
2374            self._update(out)
2375            return [self]
2376
2377        msh = Mesh(out)
2378        if must_share_edge:
2379            arr = msh.celldata["RegionId"]
2380            on = "cells"
2381        else:
2382            arr = msh.pointdata["RegionId"]
2383            on = "points"
2384
2385        alist = []
2386        for t in range(max(arr) + 1):
2387            if t == maxdepth:
2388                break
2389            suba = msh.clone().threshold("RegionId", t, t, on=on)
2390            if sort_by_area:
2391                area = suba.area()
2392            else:
2393                area = 0  # dummy
2394            suba.name = "MeshRegion" + str(t)
2395            alist.append([suba, area])
2396
2397        if sort_by_area:
2398            alist.sort(key=lambda x: x[1])
2399            alist.reverse()
2400
2401        blist = []
2402        for i, l in enumerate(alist):
2403            l[0].color(i + 1).phong()
2404            l[0].mapper.ScalarVisibilityOff()
2405            blist.append(l[0])
2406            if i < 10:
2407                l[0].pipeline = OperationNode(
2408                    f"split mesh {i}",
2409                    parents=[self],
2410                    comment=f"#pts {l[0].dataset.GetNumberOfPoints()}",
2411                )
2412        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:
2414    def extract_largest_region(self) -> Self:
2415        """
2416        Extract the largest connected part of a mesh and discard all the smaller pieces.
2417
2418        Examples:
2419            - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py)
2420        """
2421        conn = vtki.new("PolyDataConnectivityFilter")
2422        conn.SetExtractionModeToLargestRegion()
2423        conn.ScalarConnectivityOff()
2424        conn.SetInputData(self.dataset)
2425        conn.Update()
2426
2427        m = Mesh(conn.GetOutput())
2428        m.copy_properties_from(self)
2429        m.pipeline = OperationNode(
2430            "extract_largest_region",
2431            parents=[self],
2432            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2433        )
2434        m.name = "MeshLargestRegion"
2435        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:
2437    def boolean(self, operation: str, mesh2, method=0, tol=None) -> Self:
2438        """Volumetric union, intersection and subtraction of surfaces.
2439
2440        Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`.
2441
2442        Two possible algorithms are available.
2443        Setting `method` to 0 (the default) uses the boolean operation algorithm
2444        written by Cory Quammen, Chris Weigle, and Russ Taylor (https://doi.org/10.54294/216g01);
2445        setting `method` to 1 will use the "loop" boolean algorithm
2446        written by Adam Updegrove (https://doi.org/10.1016/j.advengsoft.2016.01.015).
2447
2448        Use `tol` to specify the absolute tolerance used to determine
2449        when the distance between two points is considered to be zero (defaults to 1e-6).
2450
2451        Example:
2452            - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py)
2453
2454            ![](https://vedo.embl.es/images/basic/boolean.png)
2455        """
2456        if method == 0:
2457            bf = vtki.new("BooleanOperationPolyDataFilter")
2458        elif method == 1:
2459            bf = vtki.new("LoopBooleanPolyDataFilter")
2460        else:
2461            raise ValueError(f"Unknown method={method}")
2462
2463        poly1 = self.compute_normals().dataset
2464        poly2 = mesh2.compute_normals().dataset
2465
2466        if operation.lower() in ("plus", "+"):
2467            bf.SetOperationToUnion()
2468        elif operation.lower() == "intersect":
2469            bf.SetOperationToIntersection()
2470        elif operation.lower() in ("minus", "-"):
2471            bf.SetOperationToDifference()
2472
2473        if tol:
2474            bf.SetTolerance(tol)
2475
2476        bf.SetInputData(0, poly1)
2477        bf.SetInputData(1, poly2)
2478        bf.Update()
2479
2480        msh = Mesh(bf.GetOutput(), c=None)
2481        msh.flat()
2482
2483        msh.pipeline = OperationNode(
2484            "boolean " + operation,
2485            parents=[self, mesh2],
2486            shape="cylinder",
2487            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2488        )
2489        msh.name = self.name + operation + mesh2.name
2490        return msh

Volumetric union, intersection and subtraction of surfaces.

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

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

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

Example:

def intersect_with(self, mesh2, tol=1e-06) -> Self:
2492    def intersect_with(self, mesh2, tol=1e-06) -> Self:
2493        """
2494        Intersect this Mesh with the input surface to return a set of lines.
2495
2496        Examples:
2497            - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py)
2498
2499                ![](https://vedo.embl.es/images/basic/surfIntersect.png)
2500        """
2501        bf = vtki.new("IntersectionPolyDataFilter")
2502        bf.SetGlobalWarningDisplay(0)
2503        bf.SetTolerance(tol)
2504        bf.SetInputData(0, self.dataset)
2505        bf.SetInputData(1, mesh2.dataset)
2506        bf.Update()
2507        msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off")
2508        msh.properties.SetLineWidth(3)
2509        msh.pipeline = OperationNode(
2510            "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}"
2511        )
2512        msh.name = "SurfaceIntersection"
2513        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]]:
2515    def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
2516        """
2517        Return the list of points intersecting the mesh
2518        along the segment defined by two points `p0` and `p1`.
2519
2520        Use `return_ids` to return the cell ids along with point coords
2521
2522        Example:
2523            ```python
2524            from vedo import *
2525            s = Spring()
2526            pts = s.intersect_with_line([0,0,0], [1,0.1,0])
2527            ln = Line([0,0,0], [1,0.1,0], c='blue')
2528            ps = Points(pts, r=10, c='r')
2529            show(s, ln, ps, bg='white').close()
2530            ```
2531            ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png)
2532        """
2533        if isinstance(p0, Points):
2534            p0, p1 = p0.vertices
2535
2536        if not self.line_locator:
2537            self.line_locator = vtki.new("OBBTree")
2538            self.line_locator.SetDataSet(self.dataset)
2539            if not tol:
2540                tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000
2541            self.line_locator.SetTolerance(tol)
2542            self.line_locator.BuildLocator()
2543
2544        vpts = vtki.vtkPoints()
2545        idlist = vtki.vtkIdList()
2546        self.line_locator.IntersectWithLine(p0, p1, vpts, idlist)
2547        pts = []
2548        for i in range(vpts.GetNumberOfPoints()):
2549            intersection: MutableSequence[float] = [0, 0, 0]
2550            vpts.GetPoint(i, intersection)
2551            pts.append(intersection)
2552        pts2 = np.array(pts)
2553
2554        if return_ids:
2555            pts_ids = []
2556            for i in range(idlist.GetNumberOfIds()):
2557                cid = idlist.GetId(i)
2558                pts_ids.append(cid)
2559            return (pts2, np.array(pts_ids).astype(np.uint32))
2560
2561        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:
2563    def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Self:
2564        """
2565        Intersect this Mesh with a plane to return a set of lines.
2566
2567        Example:
2568            ```python
2569            from vedo import *
2570            sph = Sphere()
2571            mi = sph.clone().intersect_with_plane().join()
2572            print(mi.lines)
2573            show(sph, mi, axes=1).close()
2574            ```
2575            ![](https://vedo.embl.es/images/feats/intersect_plane.png)
2576        """
2577        plane = vtki.new("Plane")
2578        plane.SetOrigin(origin)
2579        plane.SetNormal(normal)
2580
2581        cutter = vtki.new("PolyDataPlaneCutter")
2582        cutter.SetInputData(self.dataset)
2583        cutter.SetPlane(plane)
2584        cutter.InterpolateAttributesOn()
2585        cutter.ComputeNormalsOff()
2586        cutter.Update()
2587
2588        msh = Mesh(cutter.GetOutput())
2589        msh.c('k').lw(3).lighting("off")
2590        msh.pipeline = OperationNode(
2591            "intersect_with_plan",
2592            parents=[self],
2593            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2594        )
2595        msh.name = "PlaneIntersection"
2596        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]:
2598    def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union[Self, "vedo.Assembly"]:
2599        """
2600        Cut/clip a closed surface mesh with a collection of planes.
2601        This will produce a new closed surface by creating new polygonal
2602        faces where the input surface hits the planes.
2603
2604        The orientation of the polygons that form the surface is important.
2605        Polygons have a front face and a back face, and it's the back face that defines
2606        the interior or "solid" region of the closed surface.
2607        When a plane cuts through a "solid" region, a new cut face is generated,
2608        but not when a clipping plane cuts through a hole or "empty" region.
2609        This distinction is crucial when dealing with complex surfaces.
2610        Note that if a simple surface has its back faces pointing outwards,
2611        then that surface defines a hole in a potentially infinite solid.
2612
2613        Non-manifold surfaces should not be used with this method. 
2614
2615        Arguments:
2616            origins : (list)
2617                list of plane origins
2618            normals : (list)
2619                list of plane normals
2620            invert : (bool)
2621                invert the clipping.
2622            return_assembly : (bool)
2623                return the cap and the clipped surfaces as a `vedo.Assembly`.
2624        
2625        Example:
2626            ```python
2627            from vedo import *
2628            s = Sphere(res=50).linewidth(1)
2629            origins = [[-0.7, 0, 0], [0, -0.6, 0]]
2630            normals = [[-1, 0, 0], [0, -1, 0]]
2631            s.cut_closed_surface(origins, normals)
2632            show(s, axes=1).close()
2633            ```
2634        """        
2635        planes = vtki.new("PlaneCollection")
2636        for p, s in zip(origins, normals):
2637            plane = vtki.vtkPlane()
2638            plane.SetOrigin(vedo.utils.make3d(p))
2639            plane.SetNormal(vedo.utils.make3d(s))
2640            planes.AddItem(plane)
2641        clipper = vtki.new("ClipClosedSurface")
2642        clipper.SetInputData(self.dataset)
2643        clipper.SetClippingPlanes(planes)
2644        clipper.PassPointDataOn()
2645        clipper.GenerateFacesOn()
2646        clipper.SetScalarModeToLabels()
2647        clipper.TriangulationErrorDisplayOn()
2648        clipper.SetInsideOut(not invert)
2649
2650        if return_assembly:
2651            clipper.GenerateClipFaceOutputOn()
2652            clipper.Update()
2653            parts = []
2654            for i in range(clipper.GetNumberOfOutputPorts()):
2655                msh = Mesh(clipper.GetOutput(i))
2656                msh.copy_properties_from(self)
2657                msh.name = "CutClosedSurface"
2658                msh.pipeline = OperationNode(
2659                    "cut_closed_surface",
2660                    parents=[self],
2661                    comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2662                )
2663                parts.append(msh)
2664            asse = vedo.Assembly(parts)
2665            asse.name = "CutClosedSurface"
2666            return asse
2667
2668        else:
2669            clipper.GenerateClipFaceOutputOff()
2670            clipper.Update()
2671            self._update(clipper.GetOutput())
2672            self.flat()
2673            self.name = "CutClosedSurface"
2674            self.pipeline = OperationNode(
2675                "cut_closed_surface",
2676                parents=[self],
2677                comment=f"#pts {self.dataset.GetNumberOfPoints()}",
2678            )
2679            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]:
2681    def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Self, bool]:
2682        """
2683        Collide this Mesh with the input surface.
2684        Information is stored in `ContactCells1` and `ContactCells2`.
2685        """
2686        ipdf = vtki.new("CollisionDetectionFilter")
2687        # ipdf.SetGlobalWarningDisplay(0)
2688
2689        transform0 = vtki.vtkTransform()
2690        transform1 = vtki.vtkTransform()
2691
2692        # ipdf.SetBoxTolerance(tol)
2693        ipdf.SetCellTolerance(tol)
2694        ipdf.SetInputData(0, self.dataset)
2695        ipdf.SetInputData(1, mesh2.dataset)
2696        ipdf.SetTransform(0, transform0)
2697        ipdf.SetTransform(1, transform1)
2698        if return_bool:
2699            ipdf.SetCollisionModeToFirstContact()
2700        else:
2701            ipdf.SetCollisionModeToAllContacts()
2702        ipdf.Update()
2703
2704        if return_bool:
2705            return bool(ipdf.GetNumberOfContacts())
2706
2707        msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off")
2708        msh.metadata["ContactCells1"] = vtk2numpy(
2709            ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells")
2710        )
2711        msh.metadata["ContactCells2"] = vtk2numpy(
2712            ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells")
2713        )
2714        msh.properties.SetLineWidth(3)
2715
2716        msh.pipeline = OperationNode(
2717            "collide_with",
2718            parents=[self, mesh2],
2719            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2720        )
2721        msh.name = "SurfaceCollision"
2722        return msh

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

def geodesic(self, start, end) -> Self:
2724    def geodesic(self, start, end) -> Self:
2725        """
2726        Dijkstra algorithm to compute the geodesic line.
2727        Takes as input a polygonal mesh and performs a single source shortest path calculation.
2728
2729        The output mesh contains the array "VertexIDs" that contains the ordered list of vertices
2730        traversed to get from the start vertex to the end vertex.
2731        
2732        Arguments:
2733            start : (int, list)
2734                start vertex index or close point `[x,y,z]`
2735            end :  (int, list)
2736                end vertex index or close point `[x,y,z]`
2737
2738        Examples:
2739            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2740
2741                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2742        """
2743        if is_sequence(start):
2744            cc = self.vertices
2745            pa = Points(cc)
2746            start = pa.closest_point(start, return_point_id=True)
2747            end = pa.closest_point(end, return_point_id=True)
2748
2749        dijkstra = vtki.new("DijkstraGraphGeodesicPath")
2750        dijkstra.SetInputData(self.dataset)
2751        dijkstra.SetStartVertex(end)  # inverted in vtk
2752        dijkstra.SetEndVertex(start)
2753        dijkstra.Update()
2754
2755        weights = vtki.vtkDoubleArray()
2756        dijkstra.GetCumulativeWeights(weights)
2757
2758        idlist = dijkstra.GetIdList()
2759        ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())]
2760
2761        length = weights.GetMaxId() + 1
2762        arr = np.zeros(length)
2763        for i in range(length):
2764            arr[i] = weights.GetTuple(i)[0]
2765
2766        poly = dijkstra.GetOutput()
2767
2768        vdata = numpy2vtk(arr)
2769        vdata.SetName("CumulativeWeights")
2770        poly.GetPointData().AddArray(vdata)
2771
2772        vdata2 = numpy2vtk(ids, dtype=np.uint)
2773        vdata2.SetName("VertexIDs")
2774        poly.GetPointData().AddArray(vdata2)
2775        poly.GetPointData().Modified()
2776
2777        dmesh = Mesh(poly).copy_properties_from(self)
2778        dmesh.lw(3).alpha(1).lighting("off")
2779        dmesh.name = "GeodesicLine"
2780
2781        dmesh.pipeline = OperationNode(
2782            "GeodesicLine",
2783            parents=[self],
2784            comment=f"#steps {poly.GetNumberOfPoints()}",
2785        )
2786        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:
2791    def binarize(
2792        self,
2793        values=(255, 0),
2794        spacing=None,
2795        dims=None,
2796        origin=None,
2797    ) -> "vedo.Volume":
2798        """
2799        Convert a `Mesh` into a `Volume` where
2800        the interior voxels value is set to `values[0]` (255 by default), while
2801        the exterior voxels value is set to `values[1]` (0 by default).
2802
2803        Arguments:
2804            values : (list)
2805                background and foreground values.
2806            spacing : (list)
2807                voxel spacing in x, y and z.
2808            dims : (list)
2809                dimensions (nr. of voxels) of the output volume.
2810            origin : (list)
2811                position in space of the (0,0,0) voxel.
2812
2813        Examples:
2814            - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py)
2815
2816                ![](https://vedo.embl.es/images/volumetric/mesh2volume.png)
2817        """
2818        assert len(values) == 2, "values must be a list of 2 values"
2819        fg_value, bg_value = values
2820
2821        bounds = self.bounds()
2822        if spacing is None:  # compute spacing
2823            spacing = [0, 0, 0]
2824            diagonal = np.sqrt(
2825                  (bounds[1] - bounds[0]) ** 2
2826                + (bounds[3] - bounds[2]) ** 2
2827                + (bounds[5] - bounds[4]) ** 2
2828            )
2829            spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0
2830
2831        if dims is None:  # compute dimensions
2832            dim = [0, 0, 0]
2833            for i in [0, 1, 2]:
2834                dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i]))
2835        else:
2836            dim = dims
2837        
2838        white_img = vtki.vtkImageData()
2839        white_img.SetDimensions(dim)
2840        white_img.SetSpacing(spacing)
2841        white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1)
2842
2843        if origin is None:
2844            origin = [0, 0, 0]
2845            origin[0] = bounds[0] + spacing[0]
2846            origin[1] = bounds[2] + spacing[1]
2847            origin[2] = bounds[4] + spacing[2]
2848        white_img.SetOrigin(origin)
2849
2850        # if direction_matrix is not None:
2851        #     white_img.SetDirectionMatrix(direction_matrix)
2852
2853        white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
2854
2855        # fill the image with foreground voxels:
2856        white_img.GetPointData().GetScalars().Fill(fg_value)
2857
2858        # polygonal data --> image stencil:
2859        pol2stenc = vtki.new("PolyDataToImageStencil")
2860        pol2stenc.SetInputData(self.dataset)
2861        pol2stenc.SetOutputOrigin(white_img.GetOrigin())
2862        pol2stenc.SetOutputSpacing(white_img.GetSpacing())
2863        pol2stenc.SetOutputWholeExtent(white_img.GetExtent())
2864        pol2stenc.Update()
2865
2866        # cut the corresponding white image and set the background:
2867        imgstenc = vtki.new("ImageStencil")
2868        imgstenc.SetInputData(white_img)
2869        imgstenc.SetStencilConnection(pol2stenc.GetOutputPort())
2870        # imgstenc.SetReverseStencil(True)
2871        imgstenc.SetBackgroundValue(bg_value)
2872        imgstenc.Update()
2873
2874        vol = vedo.Volume(imgstenc.GetOutput())
2875        vol.name = "BinarizedVolume"
2876        vol.pipeline = OperationNode(
2877            "binarize",
2878            parents=[self],
2879            comment=f"dims={tuple(vol.dimensions())}",
2880            c="#e9c46a:#0096c7",
2881        )
2882        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:
2884    def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume":
2885        """
2886        Compute the `Volume` object whose voxels contains 
2887        the signed distance from the mesh.
2888
2889        Arguments:
2890            bounds : (list)
2891                bounds of the output volume
2892            dims : (list)
2893                dimensions (nr. of voxels) of the output volume
2894            invert : (bool)
2895                flip the sign
2896
2897        Examples:
2898            - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py)
2899        """
2900        if maxradius is not None:
2901            vedo.logger.warning(
2902                "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)."
2903            )
2904        if bounds is None:
2905            bounds = self.bounds()
2906        sx = (bounds[1] - bounds[0]) / dims[0]
2907        sy = (bounds[3] - bounds[2]) / dims[1]
2908        sz = (bounds[5] - bounds[4]) / dims[2]
2909
2910        img = vtki.vtkImageData()
2911        img.SetDimensions(dims)
2912        img.SetSpacing(sx, sy, sz)
2913        img.SetOrigin(bounds[0], bounds[2], bounds[4])
2914        img.AllocateScalars(vtki.VTK_FLOAT, 1)
2915
2916        imp = vtki.new("ImplicitPolyDataDistance")
2917        imp.SetInput(self.dataset)
2918        b2 = bounds[2]
2919        b4 = bounds[4]
2920        d0, d1, d2 = dims
2921
2922        for i in range(d0):
2923            x = i * sx + bounds[0]
2924            for j in range(d1):
2925                y = j * sy + b2
2926                for k in range(d2):
2927                    v = imp.EvaluateFunction((x, y, k * sz + b4))
2928                    if invert:
2929                        v = -v
2930                    img.SetScalarComponentFromFloat(i, j, k, 0, v)
2931
2932        vol = vedo.Volume(img)
2933        vol.name = "SignedVolume"
2934
2935        vol.pipeline = OperationNode(
2936            "signed_distance",
2937            parents=[self],
2938            comment=f"dims={tuple(vol.dimensions())}",
2939            c="#e9c46a:#0096c7",
2940        )
2941        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:
2943    def tetralize(
2944        self,
2945        side=0.02,
2946        nmax=300_000,
2947        gap=None,
2948        subsample=False,
2949        uniform=True,
2950        seed=0,
2951        debug=False,
2952    ) -> "vedo.TetMesh":
2953        """
2954        Tetralize a closed polygonal mesh. Return a `TetMesh`.
2955
2956        Arguments:
2957            side : (float)
2958                desired side of the single tetras as fraction of the bounding box diagonal.
2959                Typical values are in the range (0.01 - 0.03)
2960            nmax : (int)
2961                maximum random numbers to be sampled in the bounding box
2962            gap : (float)
2963                keep this minimum distance from the surface,
2964                if None an automatic choice is made.
2965            subsample : (bool)
2966                subsample input surface, the geometry might be affected
2967                (the number of original faces reduceed), but higher tet quality might be obtained.
2968            uniform : (bool)
2969                generate tets more uniformly packed in the interior of the mesh
2970            seed : (int)
2971                random number generator seed
2972            debug : (bool)
2973                show an intermediate plot with sampled points
2974
2975        Examples:
2976            - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py)
2977
2978                ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg)
2979        """
2980        surf = self.clone().clean().compute_normals()
2981        d = surf.diagonal_size()
2982        if gap is None:
2983            gap = side * d * np.sqrt(2 / 3)
2984        n = int(min((1 / side) ** 3, nmax))
2985
2986        # fill the space w/ points
2987        x0, x1, y0, y1, z0, z1 = surf.bounds()
2988
2989        if uniform:
2990            pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42)
2991            pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100  # some small jitter
2992        else:
2993            disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2
2994            np.random.seed(seed)
2995            pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp
2996
2997        normals = surf.celldata["Normals"]
2998        cc = surf.cell_centers
2999        subpts = cc - normals * gap * 1.05
3000        pts = pts.tolist() + subpts.tolist()
3001
3002        if debug:
3003            print(".. tetralize(): subsampling and cleaning")
3004
3005        fillpts = surf.inside_points(pts)
3006        fillpts.subsample(side)
3007
3008        if gap:
3009            fillpts.distance_to(surf)
3010            fillpts.threshold("Distance", above=gap)
3011
3012        if subsample:
3013            surf.subsample(side)
3014
3015        merged_fs = vedo.merge(fillpts, surf)
3016        tmesh = merged_fs.generate_delaunay3d()
3017        tcenters = tmesh.cell_centers
3018
3019        ids = surf.inside_points(tcenters, return_ids=True)
3020        ins = np.zeros(tmesh.ncells)
3021        ins[ids] = 1
3022
3023        if debug:
3024            # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close()
3025            edges = self.edges
3026            points = self.vertices
3027            elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :])
3028            histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d))
3029            print(".. edges min, max", elen.min(), elen.max())
3030            fillpts.cmap("bone")
3031            vedo.show(
3032                [
3033                    [
3034                        f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}",
3035                        surf.wireframe().alpha(0.2),
3036                        vedo.addons.Axes(surf),
3037                        fillpts,
3038                        Points(subpts).c("r4").ps(3),
3039                    ],
3040                    [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo],
3041                ],
3042                N=2,
3043                sharecam=False,
3044                new=True,
3045            ).close()
3046            print(".. thresholding")
3047
3048        tmesh.celldata["inside"] = ins.astype(np.uint8)
3049        tmesh.threshold("inside", above=0.9)
3050        tmesh.celldata.remove("inside")
3051
3052        if debug:
3053            print(f".. tetralize() completed, ntets = {tmesh.ncells}")
3054
3055        tmesh.pipeline = OperationNode(
3056            "tetralize",
3057            parents=[self],
3058            comment=f"#tets = {tmesh.ncells}",
3059            c="#e9c46a:#9e2a2b",
3060        )
3061        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
cell_edge_neighbors
map_points_to_cells
resample_data_from
interpolate_data_from
add_ids
gradient
divergence
vorticity
probe
compute_cell_size
generate_random_data
integrate_data
write
tomesh
unsigned_distance
smooth_data
compute_streamlines