vedo.mesh

Submodule to work with polygonal meshes

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

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

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

Initialize a Mesh object.

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

def faces(self, ids=()):
242    def faces(self, ids=()):
243        """DEPRECATED. Use property `mesh.cells` instead."""
244        vedo.printc("WARNING: use property mesh.cells instead of mesh.faces()",c='y')
245        return self.cells

DEPRECATED. Use property mesh.cells instead.

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

Return an array containing the edges connectivity.

cell_normals
270    @property
271    def cell_normals(self):
272        """
273        Retrieve face normals as a numpy array.
274        Check out also `compute_normals(cells=True)` and `compute_normals_with_pca()`.
275        """
276        vtknormals = self.dataset.GetCellData().GetNormals()
277        return vtk2numpy(vtknormals)

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

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

Compute cell and vertex normals for the mesh.

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

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

def reverse(self, cells=True, normals=False) -> Mesh:
315    def reverse(self, cells=True, normals=False) -> "Mesh":
316        """
317        Reverse the order of polygonal cells
318        and/or reverse the direction of point and cell normals.
319
320        Two flags are used to control these operations:
321            - `cells=True` reverses the order of the indices in the cell connectivity list.
322                If cell is a list of IDs only those cells will be reversed.
323            - `normals=True` reverses the normals by multiplying the normal vector by -1
324                (both point and cell normals, if present).
325        """
326        poly = self.dataset
327
328        if is_sequence(cells):
329            for cell in cells:
330                poly.ReverseCell(cell)
331            poly.GetCellData().Modified()
332            return self  ##############
333
334        rev = vtki.new("ReverseSense")
335        if cells:
336            rev.ReverseCellsOn()
337        else:
338            rev.ReverseCellsOff()
339        if normals:
340            rev.ReverseNormalsOn()
341        else:
342            rev.ReverseNormalsOff()
343        rev.SetInputData(poly)
344        rev.Update()
345        self._update(rev.GetOutput(), reset_locators=False)
346        self.pipeline = OperationNode("reverse", parents=[self])
347        return self

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

Two flags are used to control these operations:
  • cells=True reverses the order of the indices in the cell connectivity list. If cell is a list of IDs only those cells will be reversed.
  • normals=True reverses the normals by multiplying the normal vector by -1 (both point and cell normals, if present).
def volume(self) -> float:
349    def volume(self) -> float:
350        """Get/set the volume occupied by mesh."""
351        mass = vtki.new("MassProperties")
352        mass.SetGlobalWarningDisplay(0)
353        mass.SetInputData(self.dataset)
354        mass.Update()
355        return mass.GetVolume()

Get/set the volume occupied by mesh.

def area(self) -> float:
357    def area(self) -> float:
358        """
359        Compute the surface area of the mesh.
360        The mesh must be triangular for this to work.
361        See also `mesh.triangulate()`.
362        """
363        mass = vtki.new("MassProperties")
364        mass.SetGlobalWarningDisplay(0)
365        mass.SetInputData(self.dataset)
366        mass.Update()
367        return mass.GetSurfaceArea()

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

def is_closed(self) -> bool:
369    def is_closed(self) -> bool:
370        """Return `True` if the mesh is watertight."""
371        fe = vtki.new("FeatureEdges")
372        fe.BoundaryEdgesOn()
373        fe.FeatureEdgesOff()
374        fe.NonManifoldEdgesOn()
375        fe.SetInputData(self.dataset)
376        fe.Update()
377        ne = fe.GetOutput().GetNumberOfCells()
378        return not bool(ne)

Return True if the mesh is watertight.

def is_manifold(self) -> bool:
380    def is_manifold(self) -> bool:
381        """Return `True` if the mesh is manifold."""
382        fe = vtki.new("FeatureEdges")
383        fe.BoundaryEdgesOff()
384        fe.FeatureEdgesOff()
385        fe.NonManifoldEdgesOn()
386        fe.SetInputData(self.dataset)
387        fe.Update()
388        ne = fe.GetOutput().GetNumberOfCells()
389        return not bool(ne)

Return True if the mesh is manifold.

def non_manifold_faces(self, remove=True, tol='auto') -> Mesh:
391    def non_manifold_faces(self, remove=True, tol="auto") -> "Mesh":
392        """
393        Detect and (try to) remove non-manifold faces of a triangular mesh:
394
395            - set `remove` to `False` to mark cells without removing them.
396            - set `tol=0` for zero-tolerance, the result will be manifold but with holes.
397            - set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
398            - set `tol="auto"` to make an automatic choice of the tolerance.
399        """
400        # mark original point and cell ids
401        self.add_ids()
402        toremove = self.boundaries(
403            boundary_edges=False,
404            non_manifold_edges=True,
405            cell_edge=True,
406            return_cell_ids=True,
407        )
408        if len(toremove) == 0: # type: ignore
409            return self
410
411        points = self.vertices
412        faces = self.cells
413        centers = self.cell_centers
414
415        copy = self.clone()
416        copy.delete_cells(toremove).clean()
417        copy.compute_normals(cells=False)
418        normals = copy.vertex_normals
419        deltas, deltas_i = [], []
420
421        for i in vedo.utils.progressbar(toremove, delay=3, title="recover faces"):
422            pids = copy.closest_point(centers[i], n=3, return_point_id=True)
423            norms = normals[pids]
424            n = np.mean(norms, axis=0)
425            dn = np.linalg.norm(n)
426            if not dn:
427                continue
428            n = n / dn
429
430            p0, p1, p2 = points[faces[i]][:3]
431            v = np.cross(p1 - p0, p2 - p0)
432            lv = np.linalg.norm(v)
433            if not lv:
434                continue
435            v = v / lv
436
437            cosa = 1 - np.dot(n, v)
438            deltas.append(cosa)
439            deltas_i.append(i)
440
441        recover = []
442        if len(deltas) > 0:
443            mean_delta = np.mean(deltas)
444            err_delta = np.std(deltas)
445            txt = ""
446            if tol == "auto":  # automatic choice
447                tol = mean_delta / 5
448                txt = f"\n Automatic tol. : {tol: .4f}"
449            for i, cosa in zip(deltas_i, deltas):
450                if cosa < tol:
451                    recover.append(i)
452
453            vedo.logger.info(
454                f"\n --------- Non manifold faces ---------"
455                f"\n Average tol.   : {mean_delta: .4f} +- {err_delta: .4f}{txt}"
456                f"\n Removed faces  : {len(toremove)}" # type: ignore
457                f"\n Recovered faces: {len(recover)}"
458            )
459
460        toremove = list(set(toremove) - set(recover)) # type: ignore
461
462        if not remove:
463            mark = np.zeros(self.ncells, dtype=np.uint8)
464            mark[recover] = 1
465            mark[toremove] = 2
466            self.celldata["NonManifoldCell"] = mark
467        else:
468            self.delete_cells(toremove) # type: ignore
469
470        self.pipeline = OperationNode(
471            "non_manifold_faces",
472            parents=[self],
473            comment=f"#cells {self.dataset.GetNumberOfCells()}",
474        )
475        return self

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

- set `remove` to `False` to mark cells without removing them.
- set `tol=0` for zero-tolerance, the result will be manifold but with holes.
- set `tol>0` to cut off non-manifold faces, and try to recover the good ones.
- set `tol="auto"` to make an automatic choice of the tolerance.
def shrink(self, fraction=0.85) -> Mesh:
477    def shrink(self, fraction=0.85) -> "Mesh":
478        """Shrink the triangle polydata in the representation of the input mesh.
479
480        Examples:
481            - [shrink.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shrink.py)
482
483            ![](https://vedo.embl.es/images/basic/shrink.png)
484        """
485        # Overriding base class method core.shrink()
486        shrink = vtki.new("ShrinkPolyData")
487        shrink.SetInputData(self.dataset)
488        shrink.SetShrinkFactor(fraction)
489        shrink.Update()
490        self._update(shrink.GetOutput())
491        self.pipeline = OperationNode("shrink", parents=[self])
492        return self

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

Examples:

def cap(self, return_cap=False) -> Mesh:
494    def cap(self, return_cap=False) -> "Mesh":
495        """
496        Generate a "cap" on a clipped mesh, or caps sharp edges.
497
498        Examples:
499            - [cut_and_cap.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/cut_and_cap.py)
500
501            ![](https://vedo.embl.es/images/advanced/cutAndCap.png)
502
503        See also: `join()`, `join_segments()`, `slice()`.
504        """
505        fe = vtki.new("FeatureEdges")
506        fe.SetInputData(self.dataset)
507        fe.BoundaryEdgesOn()
508        fe.FeatureEdgesOff()
509        fe.NonManifoldEdgesOff()
510        fe.ManifoldEdgesOff()
511        fe.Update()
512
513        stripper = vtki.new("Stripper")
514        stripper.SetInputData(fe.GetOutput())
515        stripper.JoinContiguousSegmentsOn()
516        stripper.Update()
517
518        boundary_poly = vtki.vtkPolyData()
519        boundary_poly.SetPoints(stripper.GetOutput().GetPoints())
520        boundary_poly.SetPolys(stripper.GetOutput().GetLines())
521
522        rev = vtki.new("ReverseSense")
523        rev.ReverseCellsOn()
524        rev.SetInputData(boundary_poly)
525        rev.Update()
526
527        tf = vtki.new("TriangleFilter")
528        tf.SetInputData(rev.GetOutput())
529        tf.Update()
530
531        if return_cap:
532            m = Mesh(tf.GetOutput())
533            m.pipeline = OperationNode(
534                "cap", parents=[self], comment=f"#pts {m.dataset.GetNumberOfPoints()}"
535            )
536            m.name = "MeshCap"
537            return m
538
539        polyapp = vtki.new("AppendPolyData")
540        polyapp.AddInputData(self.dataset)
541        polyapp.AddInputData(tf.GetOutput())
542        polyapp.Update()
543
544        self._update(polyapp.GetOutput())
545        self.clean()
546
547        self.pipeline = OperationNode(
548            "capped", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
549        )
550        return self

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

Examples:

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

def join(self, polys=True, reset=False) -> Mesh:
552    def join(self, polys=True, reset=False) -> "Mesh":
553        """
554        Generate triangle strips and/or polylines from
555        input polygons, triangle strips, and lines.
556
557        Input polygons are assembled into triangle strips only if they are triangles;
558        other types of polygons are passed through to the output and not stripped.
559        Use mesh.triangulate() to triangulate non-triangular polygons prior to running
560        this filter if you need to strip all the data.
561
562        Also note that if triangle strips or polylines are present in the input
563        they are passed through and not joined nor extended.
564        If you wish to strip these use mesh.triangulate() to fragment the input
565        into triangles and lines prior to applying join().
566
567        Arguments:
568            polys : (bool)
569                polygonal segments will be joined if they are contiguous
570            reset : (bool)
571                reset points ordering
572
573        Warning:
574            If triangle strips or polylines exist in the input data
575            they will be passed through to the output data.
576            This filter will only construct triangle strips if triangle polygons
577            are available; and will only construct polylines if lines are available.
578
579        Example:
580            ```python
581            from vedo import *
582            c1 = Cylinder(pos=(0,0,0), r=2, height=3, axis=(1,.0,0), alpha=.1).triangulate()
583            c2 = Cylinder(pos=(0,0,2), r=1, height=2, axis=(0,.3,1), alpha=.1).triangulate()
584            intersect = c1.intersect_with(c2).join(reset=True)
585            spline = Spline(intersect).c('blue').lw(5)
586            show(c1, c2, spline, intersect.labels('id'), axes=1).close()
587            ```
588            ![](https://vedo.embl.es/images/feats/line_join.png)
589        """
590        sf = vtki.new("Stripper")
591        sf.SetPassThroughCellIds(True)
592        sf.SetPassThroughPointIds(True)
593        sf.SetJoinContiguousSegments(polys)
594        sf.SetInputData(self.dataset)
595        sf.Update()
596        if reset:
597            poly = sf.GetOutput()
598            cpd = vtki.new("CleanPolyData")
599            cpd.PointMergingOn()
600            cpd.ConvertLinesToPointsOn()
601            cpd.ConvertPolysToLinesOn()
602            cpd.ConvertStripsToPolysOn()
603            cpd.SetInputData(poly)
604            cpd.Update()
605            poly = cpd.GetOutput()
606            vpts = poly.GetCell(0).GetPoints().GetData()
607            poly.GetPoints().SetData(vpts)
608        else:
609            poly = sf.GetOutput()
610
611        self._update(poly)
612
613        self.pipeline = OperationNode(
614            "join", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
615        )
616        return self

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

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

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

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

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

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

def join_segments(self, closed=True, tol=0.001) -> list:
618    def join_segments(self, closed=True, tol=1e-03) -> list:
619        """
620        Join line segments into contiguous lines.
621        Useful to call with `triangulate()` method.
622
623        Returns:
624            list of `shapes.Lines`
625
626        Example:
627            ```python
628            from vedo import *
629            msh = Torus().alpha(0.1).wireframe()
630            intersection = msh.intersect_with_plane(normal=[1,1,1]).c('purple5')
631            slices = [s.triangulate() for s in intersection.join_segments()]
632            show(msh, intersection, merge(slices), axes=1, viewup='z')
633            ```
634            ![](https://vedo.embl.es/images/feats/join_segments.jpg)
635        """
636        vlines = []
637        for ipiece, outline in enumerate(self.split(must_share_edge=False)): # type: ignore
638
639            outline.clean()
640            pts = outline.vertices
641            if len(pts) < 3:
642                continue
643            avesize = outline.average_size()
644            lines = outline.lines
645            # print("---lines", lines, "in piece", ipiece)
646            tol = avesize / pts.shape[0] * tol
647
648            k = 0
649            joinedpts = [pts[k]]
650            for _ in range(len(pts)):
651                pk = pts[k]
652                for j, line in enumerate(lines):
653
654                    id0, id1 = line[0], line[-1]
655                    p0, p1 = pts[id0], pts[id1]
656
657                    if np.linalg.norm(p0 - pk) < tol:
658                        n = len(line)
659                        for m in range(1, n):
660                            joinedpts.append(pts[line[m]])
661                        # joinedpts.append(p1)
662                        k = id1
663                        lines.pop(j)
664                        break
665
666                    elif np.linalg.norm(p1 - pk) < tol:
667                        n = len(line)
668                        for m in reversed(range(0, n - 1)):
669                            joinedpts.append(pts[line[m]])
670                        # joinedpts.append(p0)
671                        k = id0
672                        lines.pop(j)
673                        break
674
675            if len(joinedpts) > 1:
676                newline = vedo.shapes.Line(joinedpts, closed=closed)
677                newline.clean()
678                newline.actor.SetProperty(self.properties)
679                newline.properties = self.properties
680                newline.pipeline = OperationNode(
681                    "join_segments",
682                    parents=[self],
683                    comment=f"#pts {newline.dataset.GetNumberOfPoints()}",
684                )
685                vlines.append(newline)
686
687        return vlines

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

Returns:

list of shapes.Lines

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

def join_with_strips(self, b1, closed=True) -> Mesh:
689    def join_with_strips(self, b1, closed=True) -> "Mesh":
690        """
691        Join booundary lines by creating a triangle strip between them.
692
693        Example:
694        ```python
695        from vedo import *
696        m1 = Cylinder(cap=False).boundaries()
697        m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
698        strips = m1.join_with_strips(m2)
699        show(m1, m2, strips, axes=1).close()
700        ```
701        """
702        b0 = self.clone().join()
703        b1 = b1.clone().join()
704
705        vertices0 = b0.vertices.tolist()
706        vertices1 = b1.vertices.tolist()
707
708        lines0 = b0.lines
709        lines1 = b1.lines
710        m =  len(lines0)
711        assert m == len(lines1), (
712            "lines must have the same number of points\n"
713            f"line has {m} points in b0 and {len(lines1)} in b1"
714        )
715
716        strips = []
717        points: List[Any] = []
718
719        for j in range(m):
720
721            ids0j = list(lines0[j])
722            ids1j = list(lines1[j])
723
724            n = len(ids0j)
725            assert n == len(ids1j), (
726                "lines must have the same number of points\n"
727                f"line {j} has {n} points in b0 and {len(ids1j)} in b1"
728            )
729
730            if closed:
731                ids0j.append(ids0j[0])
732                ids1j.append(ids1j[0])
733                vertices0.append(vertices0[ids0j[0]])
734                vertices1.append(vertices1[ids1j[0]])
735                n = n + 1
736
737            strip = []  # create a triangle strip
738            npt = len(points)
739            for ipt in range(n):
740                points.append(vertices0[ids0j[ipt]])
741                points.append(vertices1[ids1j[ipt]])
742
743            strip = list(range(npt, npt + 2*n))
744            strips.append(strip)
745
746        return Mesh([points, [], [], strips], c="k6")

Join booundary lines by creating a triangle strip between them.

Example:

from vedo import *
m1 = Cylinder(cap=False).boundaries()
m2 = Cylinder(cap=False).boundaries().pos(0.2,0,1)
strips = m1.join_with_strips(m2)
show(m1, m2, strips, axes=1).close()
def split_polylines(self) -> Mesh:
748    def split_polylines(self) -> "Mesh":
749        """Split polylines into separate segments."""
750        tf = vtki.new("TriangleFilter")
751        tf.SetPassLines(True)
752        tf.SetPassVerts(False)
753        tf.SetInputData(self.dataset)
754        tf.Update()
755        self._update(tf.GetOutput(), reset_locators=False)
756        self.lw(0).lighting("default").pickable()
757        self.pipeline = OperationNode(
758            "split_polylines", parents=[self], 
759            comment=f"#lines {self.dataset.GetNumberOfLines()}"
760        )
761        return self

Split polylines into separate segments.

def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Mesh:
763    def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> "Mesh":
764        """
765        Slice a mesh with a plane and fill the contour.
766
767        Example:
768            ```python
769            from vedo import *
770            msh = Mesh(dataurl+"bunny.obj").alpha(0.1).wireframe()
771            mslice = msh.slice(normal=[0,1,0.3], origin=[0,0.16,0])
772            mslice.c('purple5')
773            show(msh, mslice, axes=1)
774            ```
775            ![](https://vedo.embl.es/images/feats/mesh_slice.jpg)
776
777        See also: `join()`, `join_segments()`, `cap()`, `cut_with_plane()`.
778        """
779        intersection = self.intersect_with_plane(origin=origin, normal=normal)
780        slices = [s.triangulate() for s in intersection.join_segments()]
781        mslices = vedo.pointcloud.merge(slices)
782        if mslices:
783            mslices.name = "MeshSlice"
784            mslices.pipeline = OperationNode("slice", parents=[self], comment=f"normal = {normal}")
785        return mslices

Slice a mesh with a plane and fill the contour.

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

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

def triangulate(self, verts=True, lines=True) -> Mesh:
787    def triangulate(self, verts=True, lines=True) -> "Mesh":
788        """
789        Converts mesh polygons into triangles.
790
791        If the input mesh is only made of 2D lines (no faces) the output will be a triangulation
792        that fills the internal area. The contours may be concave, and may even contain holes,
793        i.e. a contour may contain an internal contour winding in the opposite
794        direction to indicate that it is a hole.
795
796        Arguments:
797            verts : (bool)
798                if True, break input vertex cells into individual vertex cells (one point per cell).
799                If False, the input vertex cells will be ignored.
800            lines : (bool)
801                if True, break input polylines into line segments.
802                If False, input lines will be ignored and the output will have no lines.
803        """
804        if self.dataset.GetNumberOfPolys() or self.dataset.GetNumberOfStrips():
805            # print("Using vtkTriangleFilter")
806            tf = vtki.new("TriangleFilter")
807            tf.SetPassLines(lines)
808            tf.SetPassVerts(verts)
809
810        elif self.dataset.GetNumberOfLines():
811            # print("Using vtkContourTriangulator")
812            tf = vtki.new("ContourTriangulator")
813            tf.TriangulationErrorDisplayOn()
814
815        else:
816            vedo.logger.debug("input in triangulate() seems to be void! Skip.")
817            return self
818
819        tf.SetInputData(self.dataset)
820        tf.Update()
821        self._update(tf.GetOutput(), reset_locators=False)
822        self.lw(0).lighting("default").pickable()
823
824        self.pipeline = OperationNode(
825            "triangulate", parents=[self], comment=f"#cells {self.dataset.GetNumberOfCells()}"
826        )
827        return self

Converts mesh polygons into triangles.

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

Arguments:
  • verts : (bool) if True, break input vertex cells into individual vertex cells (one point per cell). If False, the input vertex cells will be ignored.
  • lines : (bool) if True, break input polylines into line segments. If False, input lines will be ignored and the output will have no lines.
def compute_cell_vertex_count(self) -> Mesh:
829    def compute_cell_vertex_count(self) -> "Mesh":
830        """
831        Add to this mesh a cell data array containing the nr of vertices that a polygonal face has.
832        """
833        csf = vtki.new("CellSizeFilter")
834        csf.SetInputData(self.dataset)
835        csf.SetComputeArea(False)
836        csf.SetComputeVolume(False)
837        csf.SetComputeLength(False)
838        csf.SetComputeVertexCount(True)
839        csf.SetVertexCountArrayName("VertexCount")
840        csf.Update()
841        self.dataset.GetCellData().AddArray(
842            csf.GetOutput().GetCellData().GetArray("VertexCount")
843        )
844        return self

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

def compute_quality(self, metric=6) -> Mesh:
846    def compute_quality(self, metric=6) -> "Mesh":
847        """
848        Calculate metrics of quality for the elements of a triangular mesh.
849        This method adds to the mesh a cell array named "Quality".
850        See class 
851        [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html).
852
853        Arguments:
854            metric : (int)
855                type of available estimators are:
856                - EDGE RATIO, 0
857                - ASPECT RATIO, 1
858                - RADIUS RATIO, 2
859                - ASPECT FROBENIUS, 3
860                - MED ASPECT FROBENIUS, 4
861                - MAX ASPECT FROBENIUS, 5
862                - MIN_ANGLE, 6
863                - COLLAPSE RATIO, 7
864                - MAX ANGLE, 8
865                - CONDITION, 9
866                - SCALED JACOBIAN, 10
867                - SHEAR, 11
868                - RELATIVE SIZE SQUARED, 12
869                - SHAPE, 13
870                - SHAPE AND SIZE, 14
871                - DISTORTION, 15
872                - MAX EDGE RATIO, 16
873                - SKEW, 17
874                - TAPER, 18
875                - VOLUME, 19
876                - STRETCH, 20
877                - DIAGONAL, 21
878                - DIMENSION, 22
879                - ODDY, 23
880                - SHEAR AND SIZE, 24
881                - JACOBIAN, 25
882                - WARPAGE, 26
883                - ASPECT GAMMA, 27
884                - AREA, 28
885                - ASPECT BETA, 29
886
887        Examples:
888            - [meshquality.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/meshquality.py)
889
890            ![](https://vedo.embl.es/images/advanced/meshquality.png)
891        """
892        qf = vtki.new("MeshQuality")
893        qf.SetInputData(self.dataset)
894        qf.SetTriangleQualityMeasure(metric)
895        qf.SaveCellQualityOn()
896        qf.Update()
897        self._update(qf.GetOutput(), reset_locators=False)
898        self.mapper.SetScalarModeToUseCellData()
899        self.pipeline = OperationNode("compute_quality", parents=[self])
900        return self

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

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

def count_vertices(self) -> numpy.ndarray:
902    def count_vertices(self) -> np.ndarray:
903        """Count the number of vertices each cell has and return it as a numpy array"""
904        vc = vtki.new("CountVertices")
905        vc.SetInputData(self.dataset)
906        vc.SetOutputArrayName("VertexCount")
907        vc.Update()
908        varr = vc.GetOutput().GetCellData().GetArray("VertexCount")
909        return vtk2numpy(varr)

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

def check_validity(self, tol=0) -> numpy.ndarray:
911    def check_validity(self, tol=0) -> np.ndarray:
912        """
913        Return a numpy array of possible problematic faces following this convention:
914        - Valid               =  0
915        - WrongNumberOfPoints =  1
916        - IntersectingEdges   =  2
917        - IntersectingFaces   =  4
918        - NoncontiguousEdges  =  8
919        - Nonconvex           = 10
920        - OrientedIncorrectly = 20
921
922        Arguments:
923            tol : (float)
924                value is used as an epsilon for floating point
925                equality checks throughout the cell checking process.
926        """
927        vald = vtki.new("CellValidator")
928        if tol:
929            vald.SetTolerance(tol)
930        vald.SetInputData(self.dataset)
931        vald.Update()
932        varr = vald.GetOutput().GetCellData().GetArray("ValidityState")
933        return vtk2numpy(varr)

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

  • Valid = 0
  • WrongNumberOfPoints = 1
  • IntersectingEdges = 2
  • IntersectingFaces = 4
  • NoncontiguousEdges = 8
  • Nonconvex = 10
  • OrientedIncorrectly = 20
Arguments:
  • tol : (float) value is used as an epsilon for floating point equality checks throughout the cell checking process.
def compute_curvature(self, method=0) -> Mesh:
935    def compute_curvature(self, method=0) -> "Mesh":
936        """
937        Add scalars to `Mesh` that contains the curvature calculated in three different ways.
938
939        Variable `method` can be:
940        - 0 = gaussian
941        - 1 = mean curvature
942        - 2 = max curvature
943        - 3 = min curvature
944
945        Example:
946            ```python
947            from vedo import Torus
948            Torus().compute_curvature().add_scalarbar().show().close()
949            ```
950            ![](https://vedo.embl.es/images/advanced/torus_curv.png)
951        """
952        curve = vtki.new("Curvatures")
953        curve.SetInputData(self.dataset)
954        curve.SetCurvatureType(method)
955        curve.Update()
956        self._update(curve.GetOutput(), reset_locators=False)
957        self.mapper.ScalarVisibilityOn()
958        return self

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

Variable method can be:

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

def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> Mesh:
960    def compute_elevation(self, low=(0, 0, 0), high=(0, 0, 1), vrange=(0, 1)) -> "Mesh":
961        """
962        Add to `Mesh` a scalar array that contains distance along a specified direction.
963
964        Arguments:
965            low : (list)
966                one end of the line (small scalar values)
967            high : (list)
968                other end of the line (large scalar values)
969            vrange : (list)
970                set the range of the scalar
971
972        Example:
973            ```python
974            from vedo import Sphere
975            s = Sphere().compute_elevation(low=(0,0,0), high=(1,1,1))
976            s.add_scalarbar().show(axes=1).close()
977            ```
978            ![](https://vedo.embl.es/images/basic/compute_elevation.png)
979        """
980        ef = vtki.new("ElevationFilter")
981        ef.SetInputData(self.dataset)
982        ef.SetLowPoint(low)
983        ef.SetHighPoint(high)
984        ef.SetScalarRange(vrange)
985        ef.Update()
986        self._update(ef.GetOutput(), reset_locators=False)
987        self.mapper.ScalarVisibilityOn()
988        return self

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

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

def subdivide(self, n=1, method=0, mel=None) -> Mesh:
 990    def subdivide(self, n=1, method=0, mel=None) -> "Mesh":
 991        """
 992        Increase the number of vertices of a surface mesh.
 993
 994        Arguments:
 995            n : (int)
 996                number of subdivisions.
 997            method : (int)
 998                Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
 999            mel : (float)
1000                Maximum Edge Length (applicable to Adaptive method only).
1001        """
1002        triangles = vtki.new("TriangleFilter")
1003        triangles.SetInputData(self.dataset)
1004        triangles.Update()
1005        tri_mesh = triangles.GetOutput()
1006        if method == 0:
1007            sdf = vtki.new("LoopSubdivisionFilter")
1008        elif method == 1:
1009            sdf = vtki.new("LinearSubdivisionFilter")
1010        elif method == 2:
1011            sdf = vtki.new("AdaptiveSubdivisionFilter")
1012            if mel is None:
1013                mel = self.diagonal_size() / np.sqrt(self.dataset.GetNumberOfPoints()) / n
1014            sdf.SetMaximumEdgeLength(mel)
1015        elif method == 3:
1016            sdf = vtki.new("ButterflySubdivisionFilter")
1017        elif method == 4:
1018            sdf = vtki.new("DensifyPolyData")
1019        else:
1020            vedo.logger.error(f"in subdivide() unknown method {method}")
1021            raise RuntimeError()
1022
1023        if method != 2:
1024            sdf.SetNumberOfSubdivisions(n)
1025
1026        sdf.SetInputData(tri_mesh)
1027        sdf.Update()
1028
1029        self._update(sdf.GetOutput())
1030
1031        self.pipeline = OperationNode(
1032            "subdivide",
1033            parents=[self],
1034            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1035        )
1036        return self

Increase the number of vertices of a surface mesh.

Arguments:
  • n : (int) number of subdivisions.
  • method : (int) Loop(0), Linear(1), Adaptive(2), Butterfly(3), Centroid(4)
  • mel : (float) Maximum Edge Length (applicable to Adaptive method only).
def decimate( self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> Mesh:
1039    def decimate(self, fraction=0.5, n=None, preserve_volume=True, regularization=0.0) -> "Mesh":
1040        """
1041        Downsample the number of vertices in a mesh to `fraction`.
1042
1043        This filter preserves the `pointdata` of the input dataset. In previous versions
1044        of vedo, this decimation algorithm was referred to as quadric decimation.
1045
1046        Arguments:
1047            fraction : (float)
1048                the desired target of reduction.
1049            n : (int)
1050                the desired number of final points
1051                (`fraction` is recalculated based on it).
1052            preserve_volume : (bool)
1053                Decide whether to activate volume preservation which greatly
1054                reduces errors in triangle normal direction.
1055            regularization : (float)
1056                regularize the point finding algorithm so as to have better quality
1057                mesh elements at the cost of a slightly lower precision on the
1058                geometry potentially (mostly at sharp edges).
1059                Can be useful for decimating meshes that have been triangulated on noisy data.
1060
1061        Note:
1062            Setting `fraction=0.1` leaves 10% of the original number of vertices.
1063            Internally the VTK class
1064            [vtkQuadricDecimation](https://vtk.org/doc/nightly/html/classvtkQuadricDecimation.html)
1065            is used for this operation.
1066        
1067        See also: `decimate_binned()` and `decimate_pro()`.
1068        """
1069        poly = self.dataset
1070        if n:  # N = desired number of points
1071            npt = poly.GetNumberOfPoints()
1072            fraction = n / npt
1073            if fraction >= 1:
1074                return self
1075
1076        decimate = vtki.new("QuadricDecimation")
1077        decimate.SetVolumePreservation(preserve_volume)
1078        # decimate.AttributeErrorMetricOn()
1079        if regularization:
1080            decimate.SetRegularize(True)
1081            decimate.SetRegularization(regularization)
1082
1083        try:
1084            decimate.MapPointDataOn()
1085        except AttributeError:
1086            pass
1087
1088        decimate.SetTargetReduction(1 - fraction)
1089        decimate.SetInputData(poly)
1090        decimate.Update()
1091
1092        self._update(decimate.GetOutput())
1093        self.metadata["decimate_actual_fraction"] = 1 - decimate.GetActualReduction()
1094
1095        self.pipeline = OperationNode(
1096            "decimate",
1097            parents=[self],
1098            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1099        )
1100        return self

Downsample the number of vertices in a mesh to fraction.

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

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

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

See also: decimate_binned() and decimate_pro().

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) -> Mesh:
1102    def decimate_pro(
1103            self,
1104            fraction=0.5,
1105            n=None,
1106            preserve_topology=True,
1107            preserve_boundaries=True,
1108            splitting=False,
1109            splitting_angle=75,
1110            feature_angle=0,
1111            inflection_point_ratio=10,
1112            vertex_degree=0,
1113        ) -> "Mesh":
1114        """
1115        Downsample the number of vertices in a mesh to `fraction`.
1116
1117        This filter preserves the `pointdata` of the input dataset.
1118
1119        Arguments:
1120            fraction : (float)
1121                The desired target of reduction.
1122                Setting `fraction=0.1` leaves 10% of the original number of vertices.
1123            n : (int)
1124                the desired number of final points (`fraction` is recalculated based on it).
1125            preserve_topology : (bool)
1126                If on, mesh splitting and hole elimination will not occur.
1127                This may limit the maximum reduction that may be achieved.
1128            preserve_boundaries : (bool)
1129                Turn on/off the deletion of vertices on the boundary of a mesh.
1130                Control whether mesh boundaries are preserved during decimation.
1131            feature_angle : (float)
1132                Specify the angle that defines a feature.
1133                This angle is used to define what an edge is
1134                (i.e., if the surface normal between two adjacent triangles
1135                is >= FeatureAngle, an edge exists).
1136            splitting : (bool)
1137                Turn on/off the splitting of the mesh at corners,
1138                along edges, at non-manifold points, or anywhere else a split is required.
1139                Turning splitting off will better preserve the original topology of the mesh,
1140                but you may not obtain the requested reduction.
1141            splitting_angle : (float)
1142                Specify the angle that defines a sharp edge.
1143                This angle is used to control the splitting of the mesh.
1144                A split line exists when the surface normals between two edge connected triangles
1145                are >= `splitting_angle`.
1146            inflection_point_ratio : (float)
1147                An inflection point occurs when the ratio of reduction error between two iterations
1148                is greater than or equal to the `inflection_point_ratio` value.
1149            vertex_degree : (int)
1150                If the number of triangles connected to a vertex exceeds it then the vertex will be split.
1151
1152        Note:
1153            Setting `fraction=0.1` leaves 10% of the original number of vertices
1154        
1155        See also:
1156            `decimate()` and `decimate_binned()`.
1157        """
1158        poly = self.dataset
1159        if n:  # N = desired number of points
1160            npt = poly.GetNumberOfPoints()
1161            fraction = n / npt
1162            if fraction >= 1:
1163                return self
1164
1165        decimate = vtki.new("DecimatePro")
1166        decimate.SetPreserveTopology(preserve_topology)
1167        decimate.SetBoundaryVertexDeletion(preserve_boundaries)
1168        if feature_angle:
1169            decimate.SetFeatureAngle(feature_angle)
1170        decimate.SetSplitting(splitting)
1171        decimate.SetSplitAngle(splitting_angle)
1172        decimate.SetInflectionPointRatio(inflection_point_ratio)
1173        if vertex_degree:
1174            decimate.SetDegree(vertex_degree)
1175
1176        decimate.SetTargetReduction(1 - fraction)
1177        decimate.SetInputData(poly)
1178        decimate.Update()
1179        self._update(decimate.GetOutput())
1180
1181        self.pipeline = OperationNode(
1182            "decimate_pro",
1183            parents=[self],
1184            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1185        )
1186        return self

Downsample the number of vertices in a mesh to fraction.

This filter preserves the pointdata of the input dataset.

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

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

See also:

decimate() and decimate_binned().

def decimate_binned(self, divisions=(), use_clustering=False) -> Mesh:
1188    def decimate_binned(self, divisions=(), use_clustering=False) -> "Mesh":
1189        """
1190        Downsample the number of vertices in a mesh.
1191        
1192        This filter preserves the `celldata` of the input dataset,
1193        if `use_clustering=True` also the `pointdata` will be preserved in the result.
1194
1195        Arguments:
1196            divisions : (list)
1197                number of divisions along x, y and z axes.
1198            auto_adjust : (bool)
1199                if True, the number of divisions is automatically adjusted to
1200                create more uniform cells.
1201            use_clustering : (bool)
1202                use [vtkQuadricClustering](https://vtk.org/doc/nightly/html/classvtkQuadricClustering.html)
1203                instead of 
1204                [vtkBinnedDecimation](https://vtk.org/doc/nightly/html/classvtkBinnedDecimation.html).
1205        
1206        See also: `decimate()` and `decimate_pro()`.
1207        """
1208        if use_clustering:
1209            decimate = vtki.new("QuadricClustering")
1210            decimate.CopyCellDataOn()
1211        else:
1212            decimate = vtki.new("BinnedDecimation")
1213            decimate.ProducePointDataOn()
1214            decimate.ProduceCellDataOn()
1215
1216        decimate.SetInputData(self.dataset)
1217
1218        if len(divisions) == 0:
1219            decimate.SetAutoAdjustNumberOfDivisions(1)
1220        else:
1221            decimate.SetAutoAdjustNumberOfDivisions(0)
1222            decimate.SetNumberOfDivisions(divisions)
1223        decimate.Update()
1224
1225        self._update(decimate.GetOutput())
1226        self.metadata["decimate_binned_divisions"] = decimate.GetNumberOfDivisions()
1227        self.pipeline = OperationNode(
1228            "decimate_binned",
1229            parents=[self],
1230            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1231        )
1232        return self

Downsample the number of vertices in a mesh.

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

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

See also: decimate() and decimate_pro().

def generate_random_points(self, n: int, min_radius=0.0) -> vedo.pointcloud.Points:
1234    def generate_random_points(self, n: int, min_radius=0.0) -> "Points":
1235        """
1236        Generate `n` uniformly distributed random points
1237        inside the polygonal mesh.
1238
1239        A new point data array is added to the output points
1240        called "OriginalCellID" which contains the index of
1241        the cell ID in which the point was generated.
1242
1243        Arguments:
1244            n : (int)
1245                number of points to generate.
1246            min_radius: (float)
1247                impose a minimum distance between points.
1248                If `min_radius` is set to 0, the points are
1249                generated uniformly at random inside the mesh.
1250                If `min_radius` is set to a positive value,
1251                the points are generated uniformly at random
1252                inside the mesh, but points closer than `min_radius`
1253                to any other point are discarded.
1254
1255        Returns a `vedo.Points` object.
1256
1257        Note:
1258            Consider using `points.probe(msh)` or
1259            `points.interpolate_data_from(msh)`
1260            to interpolate existing mesh data onto the new points.
1261
1262        Example:
1263        ```python
1264        from vedo import *
1265        msh = Mesh(dataurl + "panther.stl").lw(2)
1266        pts = msh.generate_random_points(20000, min_radius=0.5)
1267        print("Original cell ids:", pts.pointdata["OriginalCellID"])
1268        show(pts, msh, axes=1).close()
1269        ```
1270        """
1271        cmesh = self.clone().clean().triangulate().compute_cell_size()
1272        triangles = cmesh.cells
1273        vertices = cmesh.vertices
1274        cumul = np.cumsum(cmesh.celldata["Area"])
1275
1276        out_pts = []
1277        orig_cell = []
1278        for _ in range(n):
1279            # choose a triangle based on area
1280            random_area = np.random.random() * cumul[-1]
1281            it = np.searchsorted(cumul, random_area)
1282            A, B, C = vertices[triangles[it]]
1283            # calculate the random point in the triangle
1284            r1, r2 = np.random.random(2)
1285            if r1 + r2 > 1:
1286                r1 = 1 - r1
1287                r2 = 1 - r2
1288            out_pts.append((1 - r1 - r2) * A + r1 * B + r2 * C)
1289            orig_cell.append(it)
1290        nporig_cell = np.array(orig_cell, dtype=np.uint32)
1291
1292        vpts = Points(out_pts)
1293        vpts.pointdata["OriginalCellID"] = nporig_cell
1294
1295        if min_radius > 0:
1296            vpts.subsample(min_radius, absolute=True)
1297
1298        vpts.point_size(5).color("k1")
1299        vpts.name = "RandomPoints"
1300        vpts.pipeline = OperationNode(
1301            "generate_random_points", c="#edabab", parents=[self])
1302        return vpts

Generate n uniformly distributed random points inside the polygonal mesh.

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

Arguments:
  • n : (int) number of points to generate.
  • min_radius: (float) impose a minimum distance between points. If min_radius is set to 0, the points are generated uniformly at random inside the mesh. 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]) -> Mesh:
1304    def delete_cells(self, ids: List[int]) -> "Mesh":
1305        """
1306        Remove cells from the mesh object by their ID.
1307        Points (vertices) are not removed (you may use `clean()` to remove those).
1308        """
1309        self.dataset.BuildLinks()
1310        for cid in ids:
1311            self.dataset.DeleteCell(cid)
1312        self.dataset.RemoveDeletedCells()
1313        self.dataset.Modified()
1314        self.mapper.Modified()
1315        self.pipeline = OperationNode(
1316            "delete_cells",
1317            parents=[self],
1318            comment=f"#cells {self.dataset.GetNumberOfCells()}",
1319        )
1320        return self

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

def delete_cells_by_point_index(self, indices: List[int]) -> Mesh:
1322    def delete_cells_by_point_index(self, indices: List[int]) -> "Mesh":
1323        """
1324        Delete a list of vertices identified by any of their vertex index.
1325
1326        See also `delete_cells()`.
1327
1328        Examples:
1329            - [delete_mesh_pts.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delete_mesh_pts.py)
1330
1331                ![](https://vedo.embl.es/images/basic/deleteMeshPoints.png)
1332        """
1333        cell_ids = vtki.vtkIdList()
1334        self.dataset.BuildLinks()
1335        n = 0
1336        for i in np.unique(indices):
1337            self.dataset.GetPointCells(i, cell_ids)
1338            for j in range(cell_ids.GetNumberOfIds()):
1339                self.dataset.DeleteCell(cell_ids.GetId(j))  # flag cell
1340                n += 1
1341
1342        self.dataset.RemoveDeletedCells()
1343        self.dataset.Modified()
1344        self.pipeline = OperationNode("delete_cells_by_point_index", parents=[self])
1345        return self

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

See also delete_cells().

Examples:
def collapse_edges(self, distance: float, iterations=1) -> Mesh:
1347    def collapse_edges(self, distance: float, iterations=1) -> "Mesh":
1348        """
1349        Collapse mesh edges so that are all above `distance`.
1350        
1351        Example:
1352            ```python
1353            from vedo import *
1354            np.random.seed(2)
1355            grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
1356            grid1.celldata['scalar'] = grid1.cell_centers[:,1]
1357            grid2 = grid1.clone().collapse_edges(0.1)
1358            show(grid1, grid2, N=2, axes=1)
1359            ```
1360        """
1361        for _ in range(iterations):
1362            medges = self.edges
1363            pts = self.vertices
1364            newpts = np.array(pts)
1365            moved = []
1366            for e in medges:
1367                if len(e) == 2:
1368                    id0, id1 = e
1369                    p0, p1 = pts[id0], pts[id1]
1370                    if (np.linalg.norm(p1-p0) < distance 
1371                        and id0 not in moved
1372                        and id1 not in moved
1373                    ):
1374                        p = (p0 + p1) / 2
1375                        newpts[id0] = p
1376                        newpts[id1] = p
1377                        moved += [id0, id1]
1378            self.vertices = newpts
1379            cpd = vtki.new("CleanPolyData")
1380            cpd.ConvertLinesToPointsOff()
1381            cpd.ConvertPolysToLinesOff()
1382            cpd.ConvertStripsToPolysOff()
1383            cpd.SetInputData(self.dataset)
1384            cpd.Update()
1385            self._update(cpd.GetOutput())
1386
1387        self.pipeline = OperationNode(
1388            "collapse_edges",
1389            parents=[self],
1390            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1391        )
1392        return self

Collapse mesh edges so that are all above distance.

Example:
from vedo import *
np.random.seed(2)
grid1 = Grid().add_gaussian_noise(0.8).triangulate().lw(1)
grid1.celldata['scalar'] = grid1.cell_centers[:,1]
grid2 = grid1.clone().collapse_edges(0.1)
show(grid1, grid2, N=2, axes=1)
def adjacency_list(self) -> List[set]:
1394    def adjacency_list(self) -> List[set]:
1395        """
1396        Computes the adjacency list for mesh edge-graph.
1397
1398        Returns: 
1399            a list with i-th entry being the set if indices of vertices connected by an edge to i-th vertex
1400        """
1401        inc = [set()] * self.nvertices
1402        for cell in self.cells:
1403            nc = len(cell)
1404            if nc > 1:
1405                for i in range(nc-1):
1406                    ci = cell[i]
1407                    inc[ci] = inc[ci].union({cell[i-1], cell[i+1]})
1408        return inc

Computes the adjacency list for mesh edge-graph.

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

def graph_ball(self, index, n: int) -> set:
1410    def graph_ball(self, index, n: int) -> set:
1411        """
1412        Computes the ball of radius `n` in the mesh' edge-graph metric centred in vertex `index`.
1413
1414        Arguments:
1415            index : (int)
1416                index of the vertex
1417            n : (int)
1418                radius in the graph metric
1419
1420        Returns:
1421            the set of indices of the vertices which are at most `n` edges from vertex `index`.
1422        """
1423        if n == 0:
1424            return {index}
1425        else:
1426            al = self.adjacency_list()
1427            ball = {index}
1428            i = 0
1429            while i < n and len(ball) < self.nvertices:
1430                for v in ball:
1431                    ball = ball.union(al[v])
1432                i += 1
1433            return ball

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

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

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

def smooth( self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> Mesh:
1435    def smooth(self, niter=15, pass_band=0.1, edge_angle=15, feature_angle=60, boundary=False) -> "Mesh":
1436        """
1437        Adjust mesh point positions using the so-called "Windowed Sinc" method.
1438
1439        Arguments:
1440            niter : (int)
1441                number of iterations.
1442            pass_band : (float)
1443                set the pass_band value for the windowed sinc filter.
1444            edge_angle : (float)
1445                edge angle to control smoothing along edges (either interior or boundary).
1446            feature_angle : (float)
1447                specifies the feature angle for sharp edge identification.
1448            boundary : (bool)
1449                specify if boundary should also be smoothed or kept unmodified
1450
1451        Examples:
1452            - [mesh_smoother1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/mesh_smoother1.py)
1453
1454            ![](https://vedo.embl.es/images/advanced/mesh_smoother2.png)
1455        """
1456        cl = vtki.new("CleanPolyData")
1457        cl.SetInputData(self.dataset)
1458        cl.Update()
1459        smf = vtki.new("WindowedSincPolyDataFilter")
1460        smf.SetInputData(cl.GetOutput())
1461        smf.SetNumberOfIterations(niter)
1462        smf.SetEdgeAngle(edge_angle)
1463        smf.SetFeatureAngle(feature_angle)
1464        smf.SetPassBand(pass_band)
1465        smf.NormalizeCoordinatesOn()
1466        smf.NonManifoldSmoothingOn()
1467        smf.FeatureEdgeSmoothingOn()
1468        smf.SetBoundarySmoothing(boundary)
1469        smf.Update()
1470
1471        self._update(smf.GetOutput())
1472
1473        self.pipeline = OperationNode(
1474            "smooth", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1475        )
1476        return self

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

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

def fill_holes(self, size=None) -> Mesh:
1478    def fill_holes(self, size=None) -> "Mesh":
1479        """
1480        Identifies and fills holes in the input mesh.
1481        Holes are identified by locating boundary edges, linking them together
1482        into loops, and then triangulating the resulting loops.
1483
1484        Arguments:
1485            size : (float)
1486                Approximate limit to the size of the hole that can be filled.
1487
1488        Examples:
1489            - [fillholes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fillholes.py)
1490        """
1491        fh = vtki.new("FillHolesFilter")
1492        if not size:
1493            mb = self.diagonal_size()
1494            size = mb / 10
1495        fh.SetHoleSize(size)
1496        fh.SetInputData(self.dataset)
1497        fh.Update()
1498
1499        self._update(fh.GetOutput())
1500
1501        self.pipeline = OperationNode(
1502            "fill_holes",
1503            parents=[self],
1504            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1505        )
1506        return self

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

Arguments:
  • size : (float) Approximate limit to the size of the hole that can be filled.
Examples:
def contains(self, point: tuple, tol=1e-05) -> bool:
1508    def contains(self, point: tuple, tol=1e-05) -> bool:
1509        """
1510        Return True if point is inside a polydata closed surface.
1511        
1512        Note:
1513            if you have many points to check use `inside_points()` instead.
1514        
1515        Example:
1516            ```python
1517            from vedo import *
1518            s = Sphere().c('green5').alpha(0.5)
1519            pt  = [0.1, 0.2, 0.3]
1520            print("Sphere contains", pt, s.contains(pt))
1521            show(s, Point(pt), axes=1).close()
1522            ```      
1523        """
1524        points = vtki.vtkPoints()
1525        points.InsertNextPoint(point)
1526        poly = vtki.vtkPolyData()
1527        poly.SetPoints(points)
1528        sep = vtki.new("SelectEnclosedPoints")
1529        sep.SetTolerance(tol)
1530        sep.CheckSurfaceOff()
1531        sep.SetInputData(poly)
1532        sep.SetSurfaceData(self.dataset)
1533        sep.Update()
1534        return bool(sep.IsInside(0))

Return True if point is inside a polydata closed surface.

Note:

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

Example:
from vedo import *
s = Sphere().c('green5').alpha(0.5)
pt  = [0.1, 0.2, 0.3]
print("Sphere contains", pt, s.contains(pt))
show(s, Point(pt), axes=1).close()
def inside_points( self, pts: Union[vedo.pointcloud.Points, list], invert=False, tol=1e-05, return_ids=False) -> Union[vedo.pointcloud.Points, numpy.ndarray]:
1536    def inside_points(self, pts: Union["Points", list], invert=False, tol=1e-05, return_ids=False) -> Union["Points", np.ndarray]:
1537        """
1538        Return the point cloud that is inside mesh surface as a new Points object.
1539
1540        If return_ids is True a list of IDs is returned and in addition input points
1541        are marked by a pointdata array named "IsInside".
1542
1543        Example:
1544            `print(pts.pointdata["IsInside"])`
1545
1546        Examples:
1547            - [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
1548
1549            ![](https://vedo.embl.es/images/basic/pca.png)
1550        """
1551        if isinstance(pts, Points):
1552            poly = pts.dataset
1553            ptsa = pts.vertices
1554        else:
1555            ptsa = np.asarray(pts)
1556            vpoints = vtki.vtkPoints()
1557            vpoints.SetData(numpy2vtk(ptsa, dtype=np.float32))
1558            poly = vtki.vtkPolyData()
1559            poly.SetPoints(vpoints)
1560
1561        sep = vtki.new("SelectEnclosedPoints")
1562        # sep = vtki.new("ExtractEnclosedPoints()
1563        sep.SetTolerance(tol)
1564        sep.SetInputData(poly)
1565        sep.SetSurfaceData(self.dataset)
1566        sep.SetInsideOut(invert)
1567        sep.Update()
1568
1569        varr = sep.GetOutput().GetPointData().GetArray("SelectedPoints")
1570        mask = vtk2numpy(varr).astype(bool)
1571        ids = np.array(range(len(ptsa)), dtype=int)[mask]
1572
1573        if isinstance(pts, Points):
1574            varr.SetName("IsInside")
1575            pts.dataset.GetPointData().AddArray(varr)
1576
1577        if return_ids:
1578            return ids
1579
1580        pcl = Points(ptsa[ids])
1581        pcl.name = "InsidePoints"
1582
1583        pcl.pipeline = OperationNode(
1584            "inside_points",
1585            parents=[self, ptsa],
1586            comment=f"#pts {pcl.dataset.GetNumberOfPoints()}",
1587        )
1588        return pcl

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

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

Example:

print(pts.pointdata["IsInside"])

Examples:

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[Mesh, numpy.ndarray]:
1590    def boundaries(
1591        self,
1592        boundary_edges=True,
1593        manifold_edges=False,
1594        non_manifold_edges=False,
1595        feature_angle=None,
1596        return_point_ids=False,
1597        return_cell_ids=False,
1598        cell_edge=False,
1599    ) -> Union["Mesh", np.ndarray]:
1600        """
1601        Return the boundary lines of an input mesh.
1602        Check also `vedo.core.CommonAlgorithms.mark_boundaries()` method.
1603
1604        Arguments:
1605            boundary_edges : (bool)
1606                Turn on/off the extraction of boundary edges.
1607            manifold_edges : (bool)
1608                Turn on/off the extraction of manifold edges.
1609            non_manifold_edges : (bool)
1610                Turn on/off the extraction of non-manifold edges.
1611            feature_angle : (bool)
1612                Specify the min angle btw 2 faces for extracting edges.
1613            return_point_ids : (bool)
1614                return a numpy array of point indices
1615            return_cell_ids : (bool)
1616                return a numpy array of cell indices
1617            cell_edge : (bool)
1618                set to `True` if a cell need to share an edge with
1619                the boundary line, or `False` if a single vertex is enough
1620
1621        Examples:
1622            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)
1623
1624            ![](https://vedo.embl.es/images/basic/boundaries.png)
1625        """
1626        fe = vtki.new("FeatureEdges")
1627        fe.SetBoundaryEdges(boundary_edges)
1628        fe.SetNonManifoldEdges(non_manifold_edges)
1629        fe.SetManifoldEdges(manifold_edges)
1630        try:
1631            fe.SetPassLines(True) # vtk9.2
1632        except AttributeError:
1633            pass
1634        fe.ColoringOff()
1635        fe.SetFeatureEdges(False)
1636        if feature_angle is not None:
1637            fe.SetFeatureEdges(True)
1638            fe.SetFeatureAngle(feature_angle)
1639
1640        if return_point_ids or return_cell_ids:
1641            idf = vtki.new("IdFilter")
1642            idf.SetInputData(self.dataset)
1643            idf.SetPointIdsArrayName("BoundaryIds")
1644            idf.SetPointIds(True)
1645            idf.Update()
1646
1647            fe.SetInputData(idf.GetOutput())
1648            fe.Update()
1649
1650            vid = fe.GetOutput().GetPointData().GetArray("BoundaryIds")
1651            npid = vtk2numpy(vid).astype(int)
1652
1653            if return_point_ids:
1654                return npid
1655
1656            if return_cell_ids:
1657                n = 1 if cell_edge else 0
1658                inface = []
1659                for i, face in enumerate(self.cells):
1660                    # isin = np.any([vtx in npid for vtx in face])
1661                    isin = 0
1662                    for vtx in face:
1663                        isin += int(vtx in npid)
1664                        if isin > n:
1665                            break
1666                    if isin > n:
1667                        inface.append(i)
1668                return np.array(inface).astype(int)
1669
1670            return self
1671
1672        else:
1673
1674            fe.SetInputData(self.dataset)
1675            fe.Update()
1676            msh = Mesh(fe.GetOutput(), c="p").lw(5).lighting("off")
1677            msh.name = "MeshBoundaries"
1678
1679            msh.pipeline = OperationNode(
1680                "boundaries",
1681                parents=[self],
1682                shape="octagon",
1683                comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
1684            )
1685            return msh

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

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

def imprint(self, loopline, tol=0.01) -> Mesh:
1687    def imprint(self, loopline, tol=0.01) -> "Mesh":
1688        """
1689        Imprint the contact surface of one object onto another surface.
1690
1691        Arguments:
1692            loopline : (vedo.Line)
1693                a Line object to be imprinted onto the mesh.
1694            tol : (float)
1695                projection tolerance which controls how close the imprint
1696                surface must be to the target.
1697
1698        Example:
1699            ```python
1700            from vedo import *
1701            grid = Grid()#.triangulate()
1702            circle = Circle(r=0.3, res=24).pos(0.11,0.12)
1703            line = Line(circle, closed=True, lw=4, c='r4')
1704            grid.imprint(line)
1705            show(grid, line, axes=1).close()
1706            ```
1707            ![](https://vedo.embl.es/images/feats/imprint.png)
1708        """
1709        loop = vtki.new("ContourLoopExtraction")
1710        loop.SetInputData(loopline.dataset)
1711        loop.Update()
1712
1713        clean_loop = vtki.new("CleanPolyData")
1714        clean_loop.SetInputData(loop.GetOutput())
1715        clean_loop.Update()
1716
1717        imp = vtki.new("ImprintFilter")
1718        imp.SetTargetData(self.dataset)
1719        imp.SetImprintData(clean_loop.GetOutput())
1720        imp.SetTolerance(tol)
1721        imp.BoundaryEdgeInsertionOn()
1722        imp.TriangulateOutputOn()
1723        imp.Update()
1724
1725        self._update(imp.GetOutput())
1726
1727        self.pipeline = OperationNode(
1728            "imprint",
1729            parents=[self],
1730            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1731        )
1732        return self

Imprint the contact surface of one object onto another surface.

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

def connected_vertices(self, index: int) -> List[int]:
1734    def connected_vertices(self, index: int) -> List[int]:
1735        """Find all vertices connected to an input vertex specified by its index.
1736
1737        Examples:
1738            - [connected_vtx.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/connected_vtx.py)
1739
1740            ![](https://vedo.embl.es/images/basic/connVtx.png)
1741        """
1742        poly = self.dataset
1743
1744        cell_idlist = vtki.vtkIdList()
1745        poly.GetPointCells(index, cell_idlist)
1746
1747        idxs = []
1748        for i in range(cell_idlist.GetNumberOfIds()):
1749            point_idlist = vtki.vtkIdList()
1750            poly.GetCellPoints(cell_idlist.GetId(i), point_idlist)
1751            for j in range(point_idlist.GetNumberOfIds()):
1752                idj = point_idlist.GetId(j)
1753                if idj == index:
1754                    continue
1755                if idj in idxs:
1756                    continue
1757                idxs.append(idj)
1758
1759        return idxs

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

Examples:

def extract_cells(self, ids: List[int]) -> Mesh:
1761    def extract_cells(self, ids: List[int]) -> "Mesh":
1762        """
1763        Extract a subset of cells from a mesh and return it as a new mesh.
1764        """
1765        selectCells = vtki.new("SelectionNode")
1766        selectCells.SetFieldType(vtki.get_class("SelectionNode").CELL)
1767        selectCells.SetContentType(vtki.get_class("SelectionNode").INDICES)
1768        idarr = vtki.vtkIdTypeArray()
1769        idarr.SetNumberOfComponents(1)
1770        idarr.SetNumberOfValues(len(ids))
1771        for i, v in enumerate(ids):
1772            idarr.SetValue(i, v)
1773        selectCells.SetSelectionList(idarr)
1774
1775        selection = vtki.new("Selection")
1776        selection.AddNode(selectCells)
1777
1778        extractSelection = vtki.new("ExtractSelection")
1779        extractSelection.SetInputData(0, self.dataset)
1780        extractSelection.SetInputData(1, selection)
1781        extractSelection.Update()
1782
1783        gf = vtki.new("GeometryFilter")
1784        gf.SetInputData(extractSelection.GetOutput())
1785        gf.Update()
1786        msh = Mesh(gf.GetOutput())
1787        msh.copy_properties_from(self)
1788        return msh

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

def connected_cells(self, index: int, return_ids=False) -> Union[Mesh, List[int]]:
1790    def connected_cells(self, index: int, return_ids=False) -> Union["Mesh", List[int]]:
1791        """Find all cellls connected to an input vertex specified by its index."""
1792
1793        # Find all cells connected to point index
1794        dpoly = self.dataset
1795        idlist = vtki.vtkIdList()
1796        dpoly.GetPointCells(index, idlist)
1797
1798        ids = vtki.vtkIdTypeArray()
1799        ids.SetNumberOfComponents(1)
1800        rids = []
1801        for k in range(idlist.GetNumberOfIds()):
1802            cid = idlist.GetId(k)
1803            ids.InsertNextValue(cid)
1804            rids.append(int(cid))
1805        if return_ids:
1806            return rids
1807
1808        selection_node = vtki.new("SelectionNode")
1809        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
1810        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
1811        selection_node.SetSelectionList(ids)
1812        selection = vtki.new("Selection")
1813        selection.AddNode(selection_node)
1814        extractSelection = vtki.new("ExtractSelection")
1815        extractSelection.SetInputData(0, dpoly)
1816        extractSelection.SetInputData(1, selection)
1817        extractSelection.Update()
1818        gf = vtki.new("GeometryFilter")
1819        gf.SetInputData(extractSelection.GetOutput())
1820        gf.Update()
1821        return Mesh(gf.GetOutput()).lw(1)

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

def silhouette( self, direction=None, border_edges=True, feature_angle=False) -> Mesh:
1823    def silhouette(self, direction=None, border_edges=True, feature_angle=False) -> "Mesh":
1824        """
1825        Return a new line `Mesh` which corresponds to the outer `silhouette`
1826        of the input as seen along a specified `direction`, this can also be
1827        a `vtkCamera` object.
1828
1829        Arguments:
1830            direction : (list)
1831                viewpoint direction vector.
1832                If `None` this is guessed by looking at the minimum
1833                of the sides of the bounding box.
1834            border_edges : (bool)
1835                enable or disable generation of border edges
1836            feature_angle : (float)
1837                minimal angle for sharp edges detection.
1838                If set to `False` the functionality is disabled.
1839
1840        Examples:
1841            - [silhouette1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette1.py)
1842
1843            ![](https://vedo.embl.es/images/basic/silhouette1.png)
1844        """
1845        sil = vtki.new("PolyDataSilhouette")
1846        sil.SetInputData(self.dataset)
1847        sil.SetBorderEdges(border_edges)
1848        if feature_angle is False:
1849            sil.SetEnableFeatureAngle(0)
1850        else:
1851            sil.SetEnableFeatureAngle(1)
1852            sil.SetFeatureAngle(feature_angle)
1853
1854        if direction is None and vedo.plotter_instance and vedo.plotter_instance.camera:
1855            sil.SetCamera(vedo.plotter_instance.camera)
1856            m = Mesh()
1857            m.mapper.SetInputConnection(sil.GetOutputPort())
1858
1859        elif isinstance(direction, vtki.vtkCamera):
1860            sil.SetCamera(direction)
1861            m = Mesh()
1862            m.mapper.SetInputConnection(sil.GetOutputPort())
1863
1864        elif direction == "2d":
1865            sil.SetVector(3.4, 4.5, 5.6)  # random
1866            sil.SetDirectionToSpecifiedVector()
1867            sil.Update()
1868            m = Mesh(sil.GetOutput())
1869
1870        elif is_sequence(direction):
1871            sil.SetVector(direction)
1872            sil.SetDirectionToSpecifiedVector()
1873            sil.Update()
1874            m = Mesh(sil.GetOutput())
1875        else:
1876            vedo.logger.error(f"in silhouette() unknown direction type {type(direction)}")
1877            vedo.logger.error("first render the scene with show() or specify camera/direction")
1878            return self
1879
1880        m.lw(2).c((0, 0, 0)).lighting("off")
1881        m.mapper.SetResolveCoincidentTopologyToPolygonOffset()
1882        m.pipeline = OperationNode("silhouette", parents=[self])
1883        m.name = "Silhouette"
1884        return m

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

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

def isobands(self, n=10, vmin=None, vmax=None) -> Mesh:
1886    def isobands(self, n=10, vmin=None, vmax=None) -> "Mesh":
1887        """
1888        Return a new `Mesh` representing the isobands of the active scalars.
1889        This is a new mesh where the scalar is now associated to cell faces and
1890        used to colorize the mesh.
1891
1892        Arguments:
1893            n : (int)
1894                number of isobands in the range
1895            vmin : (float)
1896                minimum of the range
1897            vmax : (float)
1898                maximum of the range
1899
1900        Examples:
1901            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
1902        """
1903        r0, r1 = self.dataset.GetScalarRange()
1904        if vmin is None:
1905            vmin = r0
1906        if vmax is None:
1907            vmax = r1
1908
1909        # --------------------------------
1910        bands = []
1911        dx = (vmax - vmin) / float(n)
1912        b = [vmin, vmin + dx / 2.0, vmin + dx]
1913        i = 0
1914        while i < n:
1915            bands.append(b)
1916            b = [b[0] + dx, b[1] + dx, b[2] + dx]
1917            i += 1
1918
1919        # annotate, use the midpoint of the band as the label
1920        lut = self.mapper.GetLookupTable()
1921        labels = []
1922        for b in bands:
1923            labels.append("{:4.2f}".format(b[1]))
1924        values = vtki.vtkVariantArray()
1925        for la in labels:
1926            values.InsertNextValue(vtki.vtkVariant(la))
1927        for i in range(values.GetNumberOfTuples()):
1928            lut.SetAnnotation(i, values.GetValue(i).ToString())
1929
1930        bcf = vtki.new("BandedPolyDataContourFilter")
1931        bcf.SetInputData(self.dataset)
1932        # Use either the minimum or maximum value for each band.
1933        for i, band in enumerate(bands):
1934            bcf.SetValue(i, band[2])
1935        # We will use an indexed lookup table.
1936        bcf.SetScalarModeToIndex()
1937        bcf.GenerateContourEdgesOff()
1938        bcf.Update()
1939        bcf.GetOutput().GetCellData().GetScalars().SetName("IsoBands")
1940
1941        m1 = Mesh(bcf.GetOutput()).compute_normals(cells=True)
1942        m1.mapper.SetLookupTable(lut)
1943        m1.mapper.SetScalarRange(lut.GetRange())
1944        m1.pipeline = OperationNode("isobands", parents=[self])
1945        m1.name = "IsoBands"
1946        return m1

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

Arguments:
  • n : (int) number of isobands in the range
  • vmin : (float) minimum of the range
  • vmax : (float) maximum of the range
Examples:
def isolines(self, n=10, vmin=None, vmax=None) -> Mesh:
1948    def isolines(self, n=10, vmin=None, vmax=None) -> "Mesh":
1949        """
1950        Return a new `Mesh` representing the isolines of the active scalars.
1951
1952        Arguments:
1953            n : (int)
1954                number of isolines in the range
1955            vmin : (float)
1956                minimum of the range
1957            vmax : (float)
1958                maximum of the range
1959
1960        Examples:
1961            - [isolines.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/isolines.py)
1962
1963            ![](https://vedo.embl.es/images/pyplot/isolines.png)
1964        """
1965        bcf = vtki.new("ContourFilter")
1966        bcf.SetInputData(self.dataset)
1967        r0, r1 = self.dataset.GetScalarRange()
1968        if vmin is None:
1969            vmin = r0
1970        if vmax is None:
1971            vmax = r1
1972        bcf.GenerateValues(n, vmin, vmax)
1973        bcf.Update()
1974        sf = vtki.new("Stripper")
1975        sf.SetJoinContiguousSegments(True)
1976        sf.SetInputData(bcf.GetOutput())
1977        sf.Update()
1978        cl = vtki.new("CleanPolyData")
1979        cl.SetInputData(sf.GetOutput())
1980        cl.Update()
1981        msh = Mesh(cl.GetOutput(), c="k").lighting("off")
1982        msh.mapper.SetResolveCoincidentTopologyToPolygonOffset()
1983        msh.pipeline = OperationNode("isolines", parents=[self])
1984        msh.name = "IsoLines"
1985        return msh

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

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

def extrude( self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> Mesh:
1987    def extrude(self, zshift=1.0, direction=(), rotation=0.0, dr=0.0, cap=True, res=1) -> "Mesh":
1988        """
1989        Sweep a polygonal data creating a "skirt" from free edges and lines, and lines from vertices.
1990        The input dataset is swept around the z-axis to create new polygonal primitives.
1991        For example, sweeping a line results in a cylindrical shell, and sweeping a circle creates a torus.
1992
1993        You can control whether the sweep of a 2D object (i.e., polygon or triangle strip)
1994        is capped with the generating geometry.
1995        Also, you can control the angle of rotation, and whether translation along the z-axis
1996        is performed along with the rotation. (Translation is useful for creating "springs").
1997        You also can adjust the radius of the generating geometry using the "dR" keyword.
1998
1999        The skirt is generated by locating certain topological features.
2000        Free edges (edges of polygons or triangle strips only used by one polygon or triangle strips)
2001        generate surfaces. This is true also of lines or polylines. Vertices generate lines.
2002
2003        This filter can be used to model axisymmetric objects like cylinders, bottles, and wine glasses;
2004        or translational/rotational symmetric objects like springs or corkscrews.
2005
2006        Arguments:
2007            zshift : (float)
2008                shift along z axis.
2009            direction : (list)
2010                extrusion direction in the xy plane. 
2011                note that zshift is forced to be the 3rd component of direction,
2012                which is therefore ignored.
2013            rotation : (float)
2014                set the angle of rotation.
2015            dr : (float)
2016                set the radius variation in absolute units.
2017            cap : (bool)
2018                enable or disable capping.
2019            res : (int)
2020                set the resolution of the generating geometry.
2021
2022        Warning:
2023            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result
2024            in two separate surfaces if capping is on, or no surface if capping is off.
2025
2026        Examples:
2027            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2028
2029            ![](https://vedo.embl.es/images/basic/extrude.png)
2030        """
2031        rf = vtki.new("RotationalExtrusionFilter")
2032        # rf = vtki.new("LinearExtrusionFilter")
2033        rf.SetInputData(self.dataset)  # must not be transformed
2034        rf.SetResolution(res)
2035        rf.SetCapping(cap)
2036        rf.SetAngle(rotation)
2037        rf.SetTranslation(zshift)
2038        rf.SetDeltaRadius(dr)
2039        rf.Update()
2040
2041        # convert triangle strips to polygonal data
2042        tris = vtki.new("TriangleFilter")
2043        tris.SetInputData(rf.GetOutput())
2044        tris.Update()
2045
2046        m = Mesh(tris.GetOutput())
2047
2048        if len(direction) > 1:
2049            p = self.pos()
2050            LT = vedo.LinearTransform()
2051            LT.translate(-p)
2052            LT.concatenate([
2053                [1, 0, direction[0]],
2054                [0, 1, direction[1]],
2055                [0, 0, 1]
2056            ])
2057            LT.translate(p)
2058            m.apply_transform(LT)
2059
2060        m.copy_properties_from(self).flat().lighting("default")
2061        m.pipeline = OperationNode(
2062            "extrude", parents=[self], 
2063            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2064        )
2065        m.name = "ExtrudedMesh"
2066        return m

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

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

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

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

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

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

Examples:

def extrude_and_trim_with( self, surface: Mesh, direction=(), strategy='all', cap=True, cap_strategy='max') -> Mesh:
2068    def extrude_and_trim_with(
2069            self,
2070            surface: "Mesh",
2071            direction=(),
2072            strategy="all",
2073            cap=True,
2074            cap_strategy="max",
2075    ) -> "Mesh":
2076        """
2077        Extrude a Mesh and trim it with an input surface mesh.
2078
2079        Arguments:
2080            surface : (Mesh)
2081                the surface mesh to trim with.
2082            direction : (list)
2083                extrusion direction in the xy plane.
2084            strategy : (str)
2085                either "boundary_edges" or "all_edges".
2086            cap : (bool)
2087                enable or disable capping.
2088            cap_strategy : (str)
2089                either "intersection", "minimum_distance", "maximum_distance", "average_distance".
2090
2091        The input Mesh is swept along a specified direction forming a "skirt"
2092        from the boundary edges 2D primitives (i.e., edges used by only one polygon);
2093        and/or from vertices and lines.
2094        The extent of the sweeping is limited by a second input: defined where
2095        the sweep intersects a user-specified surface.
2096
2097        Capping of the extrusion can be enabled.
2098        In this case the input, generating primitive is copied inplace as well
2099        as to the end of the extrusion skirt.
2100        (See warnings below on what happens if the intersecting sweep does not
2101        intersect, or partially intersects the trim surface.)
2102
2103        Note that this method operates in two fundamentally different modes
2104        based on the extrusion strategy. 
2105        If the strategy is "boundary_edges", then only the boundary edges of the input's
2106        2D primitives are extruded (verts and lines are extruded to generate lines and quads).
2107        However, if the extrusions strategy is "all_edges", then every edge of the 2D primitives
2108        is used to sweep out a quadrilateral polygon (again verts and lines are swept to produce lines and quads).
2109
2110        Warning:
2111            The extrusion direction is assumed to define an infinite line.
2112            The intersection with the trim surface is along a ray from the - to + direction,
2113            however only the first intersection is taken.
2114            Some polygonal objects have no free edges (e.g., sphere). When swept, this will result in two separate
2115            surfaces if capping is on and "boundary_edges" enabled,
2116            or no surface if capping is off and "boundary_edges" is enabled.
2117            If all the extrusion lines emanating from an extruding primitive do not intersect the trim surface,
2118            then no output for that primitive will be generated. In extreme cases, it is possible that no output
2119            whatsoever will be generated.
2120        
2121        Example:
2122            ```python
2123            from vedo import *
2124            sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
2125            circle = Circle([0,0,0], r=2, res=100).color('b6')
2126            extruded_circle = circle.extrude_and_trim_with(
2127                sphere, 
2128                direction=[0,-0.2,1],
2129                strategy="bound",
2130                cap=True,
2131                cap_strategy="intersection",
2132            )
2133            circle.lw(3).color("tomato").shift(dz=-0.1)
2134            show(circle, sphere, extruded_circle, axes=1).close()
2135            ```
2136        """
2137        trimmer = vtki.new("TrimmedExtrusionFilter")
2138        trimmer.SetInputData(self.dataset)
2139        trimmer.SetCapping(cap)
2140        trimmer.SetExtrusionDirection(direction)
2141        trimmer.SetTrimSurfaceData(surface.dataset)
2142        if "bound" in strategy:
2143            trimmer.SetExtrusionStrategyToBoundaryEdges()
2144        elif "all" in strategy:
2145            trimmer.SetExtrusionStrategyToAllEdges()
2146        else:
2147            vedo.logger.warning(f"extrude_and_trim(): unknown strategy {strategy}")
2148        # print (trimmer.GetExtrusionStrategy())
2149        
2150        if "intersect" in cap_strategy:
2151            trimmer.SetCappingStrategyToIntersection()
2152        elif "min" in cap_strategy:
2153            trimmer.SetCappingStrategyToMinimumDistance()
2154        elif "max" in cap_strategy:
2155            trimmer.SetCappingStrategyToMaximumDistance()
2156        elif "ave" in cap_strategy:
2157            trimmer.SetCappingStrategyToAverageDistance()
2158        else:
2159            vedo.logger.warning(f"extrude_and_trim(): unknown cap_strategy {cap_strategy}")
2160        # print (trimmer.GetCappingStrategy())
2161
2162        trimmer.Update()
2163
2164        m = Mesh(trimmer.GetOutput())
2165        m.copy_properties_from(self).flat().lighting("default")
2166        m.pipeline = OperationNode(
2167            "extrude_and_trim", parents=[self, surface],
2168            comment=f"#pts {m.dataset.GetNumberOfPoints()}"
2169        )
2170        m.name = "ExtrudedAndTrimmedMesh"
2171        return m

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

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

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

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

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

Warning:

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

Example:
from vedo import *
sphere = Sphere([-1,0,4]).rotate_x(25).wireframe().color('red5')
circle = Circle([0,0,0], r=2, res=100).color('b6')
extruded_circle = circle.extrude_and_trim_with(
    sphere, 
    direction=[0,-0.2,1],
    strategy="bound",
    cap=True,
    cap_strategy="intersection",
)
circle.lw(3).color("tomato").shift(dz=-0.1)
show(circle, sphere, extruded_circle, axes=1).close()
def split( self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True) -> Union[List[Mesh], Mesh]:
2173    def split(
2174        self, maxdepth=1000, flag=False, must_share_edge=False, sort_by_area=True
2175    ) -> Union[List["Mesh"], "Mesh"]:
2176        """
2177        Split a mesh by connectivity and order the pieces by increasing area.
2178
2179        Arguments:
2180            maxdepth : (int)
2181                only consider this maximum number of mesh parts.
2182            flag : (bool)
2183                if set to True return the same single object,
2184                but add a "RegionId" array to flag the mesh subparts
2185            must_share_edge : (bool)
2186                if True, mesh regions that only share single points will be split.
2187            sort_by_area : (bool)
2188                if True, sort the mesh parts by decreasing area.
2189
2190        Examples:
2191            - [splitmesh.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/splitmesh.py)
2192
2193            ![](https://vedo.embl.es/images/advanced/splitmesh.png)
2194        """
2195        pd = self.dataset
2196        if must_share_edge:
2197            if pd.GetNumberOfPolys() == 0:
2198                vedo.logger.warning("in split(): no polygons found. Skip.")
2199                return [self]
2200            cf = vtki.new("PolyDataEdgeConnectivityFilter")
2201            cf.BarrierEdgesOff()
2202        else:
2203            cf = vtki.new("PolyDataConnectivityFilter")
2204
2205        cf.SetInputData(pd)
2206        cf.SetExtractionModeToAllRegions()
2207        cf.SetColorRegions(True)
2208        cf.Update()
2209        out = cf.GetOutput()
2210
2211        if not out.GetNumberOfPoints():
2212            return [self]
2213
2214        if flag:
2215            self.pipeline = OperationNode("split mesh", parents=[self])
2216            self._update(out)
2217            return self
2218
2219        msh = Mesh(out)
2220        if must_share_edge:
2221            arr = msh.celldata["RegionId"]
2222            on = "cells"
2223        else:
2224            arr = msh.pointdata["RegionId"]
2225            on = "points"
2226
2227        alist = []
2228        for t in range(max(arr) + 1):
2229            if t == maxdepth:
2230                break
2231            suba = msh.clone().threshold("RegionId", t, t, on=on)
2232            if sort_by_area:
2233                area = suba.area()
2234            else:
2235                area = 0  # dummy
2236            suba.name = "MeshRegion" + str(t)
2237            alist.append([suba, area])
2238
2239        if sort_by_area:
2240            alist.sort(key=lambda x: x[1])
2241            alist.reverse()
2242
2243        blist = []
2244        for i, l in enumerate(alist):
2245            l[0].color(i + 1).phong()
2246            l[0].mapper.ScalarVisibilityOff()
2247            blist.append(l[0])
2248            if i < 10:
2249                l[0].pipeline = OperationNode(
2250                    f"split mesh {i}",
2251                    parents=[self],
2252                    comment=f"#pts {l[0].dataset.GetNumberOfPoints()}",
2253                )
2254        return blist

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

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

def extract_largest_region(self) -> Mesh:
2256    def extract_largest_region(self) -> "Mesh":
2257        """
2258        Extract the largest connected part of a mesh and discard all the smaller pieces.
2259
2260        Examples:
2261            - [largestregion.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/largestregion.py)
2262        """
2263        conn = vtki.new("PolyDataConnectivityFilter")
2264        conn.SetExtractionModeToLargestRegion()
2265        conn.ScalarConnectivityOff()
2266        conn.SetInputData(self.dataset)
2267        conn.Update()
2268
2269        m = Mesh(conn.GetOutput())
2270        m.copy_properties_from(self)
2271        m.pipeline = OperationNode(
2272            "extract_largest_region",
2273            parents=[self],
2274            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2275        )
2276        m.name = "MeshLargestRegion"
2277        return m

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

Examples:
def boolean(self, operation: str, mesh2, method=0, tol=None) -> Mesh:
2279    def boolean(self, operation: str, mesh2, method=0, tol=None) -> "Mesh":
2280        """Volumetric union, intersection and subtraction of surfaces.
2281
2282        Use `operation` for the allowed operations `['plus', 'intersect', 'minus']`.
2283
2284        Two possible algorithms are available by changing `method`.
2285
2286        Example:
2287            - [boolean.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boolean.py)
2288
2289            ![](https://vedo.embl.es/images/basic/boolean.png)
2290        """
2291        if method == 0:
2292            bf = vtki.new("BooleanOperationPolyDataFilter")
2293        elif method == 1:
2294            bf = vtki.new("LoopBooleanPolyDataFilter")
2295        else:
2296            raise ValueError(f"Unknown method={method}")
2297
2298        poly1 = self.compute_normals().dataset
2299        poly2 = mesh2.compute_normals().dataset
2300
2301        if operation.lower() in ("plus", "+"):
2302            bf.SetOperationToUnion()
2303        elif operation.lower() == "intersect":
2304            bf.SetOperationToIntersection()
2305        elif operation.lower() in ("minus", "-"):
2306            bf.SetOperationToDifference()
2307
2308        if tol:
2309            bf.SetTolerance(tol)
2310
2311        bf.SetInputData(0, poly1)
2312        bf.SetInputData(1, poly2)
2313        bf.Update()
2314
2315        msh = Mesh(bf.GetOutput(), c=None)
2316        msh.flat()
2317
2318        msh.pipeline = OperationNode(
2319            "boolean " + operation,
2320            parents=[self, mesh2],
2321            shape="cylinder",
2322            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2323        )
2324        msh.name = self.name + operation + mesh2.name
2325        return msh

Volumetric union, intersection and subtraction of surfaces.

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

Two possible algorithms are available by changing method.

Example:

def intersect_with(self, mesh2, tol=1e-06) -> Mesh:
2327    def intersect_with(self, mesh2, tol=1e-06) -> "Mesh":
2328        """
2329        Intersect this Mesh with the input surface to return a set of lines.
2330
2331        Examples:
2332            - [surf_intersect.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/surf_intersect.py)
2333
2334                ![](https://vedo.embl.es/images/basic/surfIntersect.png)
2335        """
2336        bf = vtki.new("IntersectionPolyDataFilter")
2337        bf.SetGlobalWarningDisplay(0)
2338        bf.SetTolerance(tol)
2339        bf.SetInputData(0, self.dataset)
2340        bf.SetInputData(1, mesh2.dataset)
2341        bf.Update()
2342        msh = Mesh(bf.GetOutput(), c="k", alpha=1).lighting("off")
2343        msh.properties.SetLineWidth(3)
2344        msh.pipeline = OperationNode(
2345            "intersect_with", parents=[self, mesh2], comment=f"#pts {msh.npoints}"
2346        )
2347        msh.name = "SurfaceIntersection"
2348        return msh

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]]:
2350    def intersect_with_line(self, p0, p1=None, return_ids=False, tol=0) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
2351        """
2352        Return the list of points intersecting the mesh
2353        along the segment defined by two points `p0` and `p1`.
2354
2355        Use `return_ids` to return the cell ids along with point coords
2356
2357        Example:
2358            ```python
2359            from vedo import *
2360            s = Spring()
2361            pts = s.intersect_with_line([0,0,0], [1,0.1,0])
2362            ln = Line([0,0,0], [1,0.1,0], c='blue')
2363            ps = Points(pts, r=10, c='r')
2364            show(s, ln, ps, bg='white').close()
2365            ```
2366            ![](https://user-images.githubusercontent.com/32848391/55967065-eee08300-5c79-11e9-8933-265e1bab9f7e.png)
2367        """
2368        if isinstance(p0, Points):
2369            p0, p1 = p0.vertices
2370
2371        if not self.line_locator:
2372            self.line_locator = vtki.new("OBBTree")
2373            self.line_locator.SetDataSet(self.dataset)
2374            if not tol:
2375                tol = mag(np.asarray(p1) - np.asarray(p0)) / 10000
2376            self.line_locator.SetTolerance(tol)
2377            self.line_locator.BuildLocator()
2378
2379        vpts = vtki.vtkPoints()
2380        idlist = vtki.vtkIdList()
2381        self.line_locator.IntersectWithLine(p0, p1, vpts, idlist)
2382        pts = []
2383        for i in range(vpts.GetNumberOfPoints()):
2384            intersection: MutableSequence[float] = [0, 0, 0]
2385            vpts.GetPoint(i, intersection)
2386            pts.append(intersection)
2387        pts2 = np.array(pts)
2388
2389        if return_ids:
2390            pts_ids = []
2391            for i in range(idlist.GetNumberOfIds()):
2392                cid = idlist.GetId(i)
2393                pts_ids.append(cid)
2394            return (pts2, np.array(pts_ids).astype(np.uint32))
2395
2396        return pts2

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

Use return_ids to return the cell ids along with point coords

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

def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> Mesh:
2398    def intersect_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> "Mesh":
2399        """
2400        Intersect this Mesh with a plane to return a set of lines.
2401
2402        Example:
2403            ```python
2404            from vedo import *
2405            sph = Sphere()
2406            mi = sph.clone().intersect_with_plane().join()
2407            print(mi.lines)
2408            show(sph, mi, axes=1).close()
2409            ```
2410            ![](https://vedo.embl.es/images/feats/intersect_plane.png)
2411        """
2412        plane = vtki.new("Plane")
2413        plane.SetOrigin(origin)
2414        plane.SetNormal(normal)
2415
2416        cutter = vtki.new("PolyDataPlaneCutter")
2417        cutter.SetInputData(self.dataset)
2418        cutter.SetPlane(plane)
2419        cutter.InterpolateAttributesOn()
2420        cutter.ComputeNormalsOff()
2421        cutter.Update()
2422
2423        msh = Mesh(cutter.GetOutput())
2424        msh.c('k').lw(3).lighting("off")
2425        msh.pipeline = OperationNode(
2426            "intersect_with_plan",
2427            parents=[self],
2428            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2429        )
2430        msh.name = "PlaneIntersection"
2431        return msh

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

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

def cut_closed_surface( self, origins, normals, invert=False, return_assembly=False) -> Union[Mesh, vedo.assembly.Assembly]:
2433    def cut_closed_surface(self, origins, normals, invert=False, return_assembly=False) -> Union["Mesh", "vedo.Assembly"]:
2434        """
2435        Cut/clip a closed surface mesh with a collection of planes.
2436        This will produce a new closed surface by creating new polygonal
2437        faces where the input surface hits the planes.
2438
2439        The orientation of the polygons that form the surface is important.
2440        Polygons have a front face and a back face, and it's the back face that defines
2441        the interior or "solid" region of the closed surface.
2442        When a plane cuts through a "solid" region, a new cut face is generated,
2443        but not when a clipping plane cuts through a hole or "empty" region.
2444        This distinction is crucial when dealing with complex surfaces.
2445        Note that if a simple surface has its back faces pointing outwards,
2446        then that surface defines a hole in a potentially infinite solid.
2447
2448        Non-manifold surfaces should not be used with this method. 
2449
2450        Arguments:
2451            origins : (list)
2452                list of plane origins
2453            normals : (list)
2454                list of plane normals
2455            invert : (bool)
2456                invert the clipping.
2457            return_assembly : (bool)
2458                return the cap and the clipped surfaces as a `vedo.Assembly`.
2459        
2460        Example:
2461            ```python
2462            from vedo import *
2463            s = Sphere(res=50).linewidth(1)
2464            origins = [[-0.7, 0, 0], [0, -0.6, 0]]
2465            normals = [[-1, 0, 0], [0, -1, 0]]
2466            s.cut_closed_surface(origins, normals)
2467            show(s, axes=1).close()
2468            ```
2469        """        
2470        planes = vtki.new("PlaneCollection")
2471        for p, s in zip(origins, normals):
2472            plane = vtki.vtkPlane()
2473            plane.SetOrigin(vedo.utils.make3d(p))
2474            plane.SetNormal(vedo.utils.make3d(s))
2475            planes.AddItem(plane)
2476        clipper = vtki.new("ClipClosedSurface")
2477        clipper.SetInputData(self.dataset)
2478        clipper.SetClippingPlanes(planes)
2479        clipper.PassPointDataOn()
2480        clipper.GenerateFacesOn()
2481        clipper.SetScalarModeToLabels()
2482        clipper.TriangulationErrorDisplayOn()
2483        clipper.SetInsideOut(not invert)
2484
2485        if return_assembly:
2486            clipper.GenerateClipFaceOutputOn()
2487            clipper.Update()
2488            parts = []
2489            for i in range(clipper.GetNumberOfOutputPorts()):
2490                msh = Mesh(clipper.GetOutput(i))
2491                msh.copy_properties_from(self)
2492                msh.name = "CutClosedSurface"
2493                msh.pipeline = OperationNode(
2494                    "cut_closed_surface",
2495                    parents=[self],
2496                    comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2497                )
2498                parts.append(msh)
2499            asse = vedo.Assembly(parts)
2500            asse.name = "CutClosedSurface"
2501            return asse
2502
2503        else:
2504            clipper.GenerateClipFaceOutputOff()
2505            clipper.Update()
2506            self._update(clipper.GetOutput())
2507            self.flat()
2508            self.name = "CutClosedSurface"
2509            self.pipeline = OperationNode(
2510                "cut_closed_surface",
2511                parents=[self],
2512                comment=f"#pts {self.dataset.GetNumberOfPoints()}",
2513            )
2514            return self

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

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

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

Arguments:
  • origins : (list) list of plane origins
  • normals : (list) list of plane normals
  • invert : (bool) invert the clipping.
  • return_assembly : (bool) return the cap and the clipped surfaces as a vedo.Assembly.
Example:
from vedo import *
s = Sphere(res=50).linewidth(1)
origins = [[-0.7, 0, 0], [0, -0.6, 0]]
normals = [[-1, 0, 0], [0, -1, 0]]
s.cut_closed_surface(origins, normals)
show(s, axes=1).close()
def collide_with(self, mesh2, tol=0, return_bool=False) -> Union[Mesh, bool]:
2516    def collide_with(self, mesh2, tol=0, return_bool=False) -> Union["Mesh", bool]:
2517        """
2518        Collide this Mesh with the input surface.
2519        Information is stored in `ContactCells1` and `ContactCells2`.
2520        """
2521        ipdf = vtki.new("CollisionDetectionFilter")
2522        # ipdf.SetGlobalWarningDisplay(0)
2523
2524        transform0 = vtki.vtkTransform()
2525        transform1 = vtki.vtkTransform()
2526
2527        # ipdf.SetBoxTolerance(tol)
2528        ipdf.SetCellTolerance(tol)
2529        ipdf.SetInputData(0, self.dataset)
2530        ipdf.SetInputData(1, mesh2.dataset)
2531        ipdf.SetTransform(0, transform0)
2532        ipdf.SetTransform(1, transform1)
2533        if return_bool:
2534            ipdf.SetCollisionModeToFirstContact()
2535        else:
2536            ipdf.SetCollisionModeToAllContacts()
2537        ipdf.Update()
2538
2539        if return_bool:
2540            return bool(ipdf.GetNumberOfContacts())
2541
2542        msh = Mesh(ipdf.GetContactsOutput(), "k", 1).lighting("off")
2543        msh.metadata["ContactCells1"] = vtk2numpy(
2544            ipdf.GetOutput(0).GetFieldData().GetArray("ContactCells")
2545        )
2546        msh.metadata["ContactCells2"] = vtk2numpy(
2547            ipdf.GetOutput(1).GetFieldData().GetArray("ContactCells")
2548        )
2549        msh.properties.SetLineWidth(3)
2550
2551        msh.pipeline = OperationNode(
2552            "collide_with",
2553            parents=[self, mesh2],
2554            comment=f"#pts {msh.dataset.GetNumberOfPoints()}",
2555        )
2556        msh.name = "SurfaceCollision"
2557        return msh

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

def geodesic(self, start, end) -> Mesh:
2559    def geodesic(self, start, end) -> "Mesh":
2560        """
2561        Dijkstra algorithm to compute the geodesic line.
2562        Takes as input a polygonal mesh and performs a single source shortest path calculation.
2563
2564        The output mesh contains the array "VertexIDs" that contains the ordered list of vertices
2565        traversed to get from the start vertex to the end vertex.
2566        
2567        Arguments:
2568            start : (int, list)
2569                start vertex index or close point `[x,y,z]`
2570            end :  (int, list)
2571                end vertex index or close point `[x,y,z]`
2572
2573        Examples:
2574            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2575
2576                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2577        """
2578        if is_sequence(start):
2579            cc = self.vertices
2580            pa = Points(cc)
2581            start = pa.closest_point(start, return_point_id=True)
2582            end = pa.closest_point(end, return_point_id=True)
2583
2584        dijkstra = vtki.new("DijkstraGraphGeodesicPath")
2585        dijkstra.SetInputData(self.dataset)
2586        dijkstra.SetStartVertex(end)  # inverted in vtk
2587        dijkstra.SetEndVertex(start)
2588        dijkstra.Update()
2589
2590        weights = vtki.vtkDoubleArray()
2591        dijkstra.GetCumulativeWeights(weights)
2592
2593        idlist = dijkstra.GetIdList()
2594        ids = [idlist.GetId(i) for i in range(idlist.GetNumberOfIds())]
2595
2596        length = weights.GetMaxId() + 1
2597        arr = np.zeros(length)
2598        for i in range(length):
2599            arr[i] = weights.GetTuple(i)[0]
2600
2601        poly = dijkstra.GetOutput()
2602
2603        vdata = numpy2vtk(arr)
2604        vdata.SetName("CumulativeWeights")
2605        poly.GetPointData().AddArray(vdata)
2606
2607        vdata2 = numpy2vtk(ids, dtype=np.uint)
2608        vdata2.SetName("VertexIDs")
2609        poly.GetPointData().AddArray(vdata2)
2610        poly.GetPointData().Modified()
2611
2612        dmesh = Mesh(poly).copy_properties_from(self)
2613        dmesh.lw(3).alpha(1).lighting("off")
2614        dmesh.name = "GeodesicLine"
2615
2616        dmesh.pipeline = OperationNode(
2617            "GeodesicLine",
2618            parents=[self],
2619            comment=f"#steps {poly.GetNumberOfPoints()}",
2620        )
2621        return dmesh

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

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

Arguments:
  • start : (int, list) start vertex index or close point [x,y,z]
  • end : (int, list) end vertex index or close point [x,y,z]
Examples:
def binarize( self, values=(255, 0), spacing=None, dims=None, origin=None) -> vedo.volume.Volume:
2626    def binarize(
2627        self,
2628        values=(255, 0),
2629        spacing=None,
2630        dims=None,
2631        origin=None,
2632    ) -> "vedo.Volume":
2633        """
2634        Convert a `Mesh` into a `Volume` where
2635        the interior voxels value is set to `values[0]` (255 by default), while
2636        the exterior voxels value is set to `values[1]` (0 by default).
2637
2638        Arguments:
2639            values : (list)
2640                background and foreground values.
2641            spacing : (list)
2642                voxel spacing in x, y and z.
2643            dims : (list)
2644                dimensions (nr. of voxels) of the output volume.
2645            origin : (list)
2646                position in space of the (0,0,0) voxel.
2647
2648        Examples:
2649            - [mesh2volume.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/mesh2volume.py)
2650
2651                ![](https://vedo.embl.es/images/volumetric/mesh2volume.png)
2652        """
2653        assert len(values) == 2, "values must be a list of 2 values"
2654        fg_value, bg_value = values
2655
2656        bounds = self.bounds()
2657        if spacing is None:  # compute spacing
2658            spacing = [0, 0, 0]
2659            diagonal = np.sqrt(
2660                  (bounds[1] - bounds[0]) ** 2
2661                + (bounds[3] - bounds[2]) ** 2
2662                + (bounds[5] - bounds[4]) ** 2
2663            )
2664            spacing[0] = spacing[1] = spacing[2] = diagonal / 250.0
2665
2666        if dims is None:  # compute dimensions
2667            dim = [0, 0, 0]
2668            for i in [0, 1, 2]:
2669                dim[i] = int(np.ceil((bounds[i*2+1] - bounds[i*2]) / spacing[i]))
2670        else:
2671            dim = dims
2672        
2673        white_img = vtki.vtkImageData()
2674        white_img.SetDimensions(dim)
2675        white_img.SetSpacing(spacing)
2676        white_img.SetExtent(0, dim[0]-1, 0, dim[1]-1, 0, dim[2]-1)
2677
2678        if origin is None:
2679            origin = [0, 0, 0]
2680            origin[0] = bounds[0] + spacing[0]
2681            origin[1] = bounds[2] + spacing[1]
2682            origin[2] = bounds[4] + spacing[2]
2683        white_img.SetOrigin(origin)
2684
2685        # if direction_matrix is not None:
2686        #     white_img.SetDirectionMatrix(direction_matrix)
2687
2688        white_img.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
2689
2690        # fill the image with foreground voxels:
2691        white_img.GetPointData().GetScalars().Fill(fg_value)
2692
2693        # polygonal data --> image stencil:
2694        pol2stenc = vtki.new("PolyDataToImageStencil")
2695        pol2stenc.SetInputData(self.dataset)
2696        pol2stenc.SetOutputOrigin(white_img.GetOrigin())
2697        pol2stenc.SetOutputSpacing(white_img.GetSpacing())
2698        pol2stenc.SetOutputWholeExtent(white_img.GetExtent())
2699        pol2stenc.Update()
2700
2701        # cut the corresponding white image and set the background:
2702        imgstenc = vtki.new("ImageStencil")
2703        imgstenc.SetInputData(white_img)
2704        imgstenc.SetStencilConnection(pol2stenc.GetOutputPort())
2705        # imgstenc.SetReverseStencil(True)
2706        imgstenc.SetBackgroundValue(bg_value)
2707        imgstenc.Update()
2708
2709        vol = vedo.Volume(imgstenc.GetOutput())
2710        vol.name = "BinarizedVolume"
2711        vol.pipeline = OperationNode(
2712            "binarize",
2713            parents=[self],
2714            comment=f"dims={tuple(vol.dimensions())}",
2715            c="#e9c46a:#0096c7",
2716        )
2717        return vol

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

Arguments:
  • values : (list) background and foreground values.
  • spacing : (list) voxel spacing in x, y and z.
  • dims : (list) dimensions (nr. of voxels) of the output volume.
  • origin : (list) position in space of the (0,0,0) voxel.
Examples:
def signed_distance( self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> vedo.volume.Volume:
2719    def signed_distance(self, bounds=None, dims=(20, 20, 20), invert=False, maxradius=None) -> "vedo.Volume":
2720        """
2721        Compute the `Volume` object whose voxels contains 
2722        the signed distance from the mesh.
2723
2724        Arguments:
2725            bounds : (list)
2726                bounds of the output volume
2727            dims : (list)
2728                dimensions (nr. of voxels) of the output volume
2729            invert : (bool)
2730                flip the sign
2731
2732        Examples:
2733            - [volume_from_mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_from_mesh.py)
2734        """
2735        if maxradius is not None:
2736            vedo.logger.warning(
2737                "in signedDistance(maxradius=...) is ignored. (Only valid for pointclouds)."
2738            )
2739        if bounds is None:
2740            bounds = self.bounds()
2741        sx = (bounds[1] - bounds[0]) / dims[0]
2742        sy = (bounds[3] - bounds[2]) / dims[1]
2743        sz = (bounds[5] - bounds[4]) / dims[2]
2744
2745        img = vtki.vtkImageData()
2746        img.SetDimensions(dims)
2747        img.SetSpacing(sx, sy, sz)
2748        img.SetOrigin(bounds[0], bounds[2], bounds[4])
2749        img.AllocateScalars(vtki.VTK_FLOAT, 1)
2750
2751        imp = vtki.new("ImplicitPolyDataDistance")
2752        imp.SetInput(self.dataset)
2753        b2 = bounds[2]
2754        b4 = bounds[4]
2755        d0, d1, d2 = dims
2756
2757        for i in range(d0):
2758            x = i * sx + bounds[0]
2759            for j in range(d1):
2760                y = j * sy + b2
2761                for k in range(d2):
2762                    v = imp.EvaluateFunction((x, y, k * sz + b4))
2763                    if invert:
2764                        v = -v
2765                    img.SetScalarComponentFromFloat(i, j, k, 0, v)
2766
2767        vol = vedo.Volume(img)
2768        vol.name = "SignedVolume"
2769
2770        vol.pipeline = OperationNode(
2771            "signed_distance",
2772            parents=[self],
2773            comment=f"dims={tuple(vol.dimensions())}",
2774            c="#e9c46a:#0096c7",
2775        )
2776        return vol

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

Arguments:
  • bounds : (list) bounds of the output volume
  • dims : (list) dimensions (nr. of voxels) of the output volume
  • invert : (bool) flip the sign
Examples:
def tetralize( self, side=0.02, nmax=300000, gap=None, subsample=False, uniform=True, seed=0, debug=False) -> vedo.grids.TetMesh:
2778    def tetralize(
2779        self,
2780        side=0.02,
2781        nmax=300_000,
2782        gap=None,
2783        subsample=False,
2784        uniform=True,
2785        seed=0,
2786        debug=False,
2787    ) -> "vedo.TetMesh":
2788        """
2789        Tetralize a closed polygonal mesh. Return a `TetMesh`.
2790
2791        Arguments:
2792            side : (float)
2793                desired side of the single tetras as fraction of the bounding box diagonal.
2794                Typical values are in the range (0.01 - 0.03)
2795            nmax : (int)
2796                maximum random numbers to be sampled in the bounding box
2797            gap : (float)
2798                keep this minimum distance from the surface,
2799                if None an automatic choice is made.
2800            subsample : (bool)
2801                subsample input surface, the geometry might be affected
2802                (the number of original faces reduceed), but higher tet quality might be obtained.
2803            uniform : (bool)
2804                generate tets more uniformly packed in the interior of the mesh
2805            seed : (int)
2806                random number generator seed
2807            debug : (bool)
2808                show an intermediate plot with sampled points
2809
2810        Examples:
2811            - [tetralize_surface.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tetralize_surface.py)
2812
2813                ![](https://vedo.embl.es/images/volumetric/tetralize_surface.jpg)
2814        """
2815        surf = self.clone().clean().compute_normals()
2816        d = surf.diagonal_size()
2817        if gap is None:
2818            gap = side * d * np.sqrt(2 / 3)
2819        n = int(min((1 / side) ** 3, nmax))
2820
2821        # fill the space w/ points
2822        x0, x1, y0, y1, z0, z1 = surf.bounds()
2823
2824        if uniform:
2825            pts = vedo.utils.pack_spheres([x0, x1, y0, y1, z0, z1], side * d * 1.42)
2826            pts += np.random.randn(len(pts), 3) * side * d * 1.42 / 100  # some small jitter
2827        else:
2828            disp = np.array([x0 + x1, y0 + y1, z0 + z1]) / 2
2829            np.random.seed(seed)
2830            pts = (np.random.rand(n, 3) - 0.5) * np.array([x1 - x0, y1 - y0, z1 - z0]) + disp
2831
2832        normals = surf.celldata["Normals"]
2833        cc = surf.cell_centers
2834        subpts = cc - normals * gap * 1.05
2835        pts = pts.tolist() + subpts.tolist()
2836
2837        if debug:
2838            print(".. tetralize(): subsampling and cleaning")
2839
2840        fillpts = surf.inside_points(pts)
2841        fillpts.subsample(side)
2842
2843        if gap:
2844            fillpts.distance_to(surf)
2845            fillpts.threshold("Distance", above=gap)
2846
2847        if subsample:
2848            surf.subsample(side)
2849
2850        merged_fs = vedo.merge(fillpts, surf)
2851        tmesh = merged_fs.generate_delaunay3d()
2852        tcenters = tmesh.cell_centers
2853
2854        ids = surf.inside_points(tcenters, return_ids=True)
2855        ins = np.zeros(tmesh.ncells)
2856        ins[ids] = 1
2857
2858        if debug:
2859            # vedo.pyplot.histogram(fillpts.pointdata["Distance"], xtitle=f"gap={gap}").show().close()
2860            edges = self.edges
2861            points = self.vertices
2862            elen = mag(points[edges][:, 0, :] - points[edges][:, 1, :])
2863            histo = vedo.pyplot.histogram(elen, xtitle="edge length", xlim=(0, 3 * side * d))
2864            print(".. edges min, max", elen.min(), elen.max())
2865            fillpts.cmap("bone")
2866            vedo.show(
2867                [
2868                    [
2869                        f"This is a debug plot.\n\nGenerated points: {n}\ngap: {gap}",
2870                        surf.wireframe().alpha(0.2),
2871                        vedo.addons.Axes(surf),
2872                        fillpts,
2873                        Points(subpts).c("r4").ps(3),
2874                    ],
2875                    [f"Edges mean length: {np.mean(elen)}\n\nPress q to continue", histo],
2876                ],
2877                N=2,
2878                sharecam=False,
2879                new=True,
2880            ).close()
2881            print(".. thresholding")
2882
2883        tmesh.celldata["inside"] = ins.astype(np.uint8)
2884        tmesh.threshold("inside", above=0.9)
2885        tmesh.celldata.remove("inside")
2886
2887        if debug:
2888            print(f".. tetralize() completed, ntets = {tmesh.ncells}")
2889
2890        tmesh.pipeline = OperationNode(
2891            "tetralize",
2892            parents=[self],
2893            comment=f"#tets = {tmesh.ncells}",
2894            c="#e9c46a:#9e2a2b",
2895        )
2896        return tmesh

Tetralize a closed polygonal mesh. Return a TetMesh.

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