vedo.grids

Work with tetrahedral meshes.

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import os
   4import time
   5from weakref import ref as weak_ref_to
   6from typing import Any
   7from typing_extensions import Self
   8import numpy as np
   9
  10import vedo.vtkclasses as vtki  # a wrapper for lazy imports
  11
  12import vedo
  13from vedo import utils
  14from vedo.core import PointAlgorithms
  15from vedo.mesh import Mesh
  16from vedo.file_io import download
  17from vedo.visual import MeshVisual
  18from vedo.transformations import LinearTransform
  19
  20__docformat__ = "google"
  21
  22__doc__ = """
  23Work with tetrahedral meshes.
  24
  25![](https://vedo.embl.es/images/volumetric/82767107-2631d500-9e25-11ea-967c-42558f98f721.jpg)
  26"""
  27
  28__all__ = [
  29    "UnstructuredGrid",
  30    "TetMesh",
  31    "RectilinearGrid",
  32    "StructuredGrid",
  33]
  34
  35
  36#########################################################################
  37class UnstructuredGrid(PointAlgorithms, MeshVisual):
  38    """Support for UnstructuredGrid objects."""
  39
  40    def __init__(self, inputobj=None):
  41        """
  42        Support for UnstructuredGrid objects.
  43
  44        Arguments:
  45            inputobj : (list, vtkUnstructuredGrid, str)
  46                A list in the form `[points, cells, celltypes]`,
  47                or a vtkUnstructuredGrid object, or a filename
  48
  49        Celltypes are identified by the following 
  50        [convention](https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html).
  51        """
  52        super().__init__()
  53
  54        self.dataset = None
  55
  56        self.mapper = vtki.new("PolyDataMapper")
  57        self._actor = vtki.vtkActor()
  58        self._actor.retrieve_object = weak_ref_to(self)
  59        self._actor.SetMapper(self.mapper)
  60        self.properties = self._actor.GetProperty()
  61
  62        self.transform = LinearTransform()
  63        self.point_locator = None
  64        self.cell_locator = None
  65        self.line_locator = None
  66
  67        self.name = "UnstructuredGrid"
  68        self.filename = ""
  69        self.file_size = ""
  70
  71        self.info = {}
  72        self.time = time.time()
  73        self.rendered_at = set()
  74
  75        ######################################
  76        inputtype = str(type(inputobj))
  77
  78        if inputobj is None:
  79            self.dataset = vtki.vtkUnstructuredGrid()
  80
  81        elif utils.is_sequence(inputobj):
  82
  83            pts, cells, celltypes = inputobj
  84            assert len(cells) == len(celltypes)
  85
  86            self.dataset = vtki.vtkUnstructuredGrid()
  87
  88            if not utils.is_sequence(cells[0]):
  89                tets = []
  90                nf = cells[0] + 1
  91                for i, cl in enumerate(cells):
  92                    if i in (nf, 0):
  93                        k = i + 1
  94                        nf = cl + k
  95                        cell = [cells[j + k] for j in range(cl)]
  96                        tets.append(cell)
  97                cells = tets
  98
  99            # This would fill the points and use those to define orientation
 100            vpts = utils.numpy2vtk(pts, dtype=np.float32)
 101            points = vtki.vtkPoints()
 102            points.SetData(vpts)
 103            self.dataset.SetPoints(points)
 104
 105            # Fill cells
 106            # https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html
 107            for i, ct in enumerate(celltypes):
 108                if   ct == vtki.cell_types["VERTEX"]:
 109                    cell = vtki.vtkVertex()
 110                elif ct == vtki.cell_types["POLY_VERTEX"]:
 111                    cell = vtki.vtkPolyVertex()
 112                elif ct == vtki.cell_types["TETRA"]:
 113                    cell = vtki.vtkTetra()
 114                elif ct == vtki.cell_types["WEDGE"]:
 115                    cell = vtki.vtkWedge()
 116                elif ct == vtki.cell_types["LINE"]:
 117                    cell = vtki.vtkLine()
 118                elif ct == vtki.cell_types["POLY_LINE"]:
 119                    cell = vtki.vtkPolyLine()
 120                elif ct == vtki.cell_types["TRIANGLE"]:
 121                    cell = vtki.vtkTriangle()
 122                elif ct == vtki.cell_types["TRIANGLE_STRIP"]:
 123                    cell = vtki.vtkTriangleStrip()
 124                elif ct == vtki.cell_types["POLYGON"]:
 125                    cell = vtki.vtkPolygon()
 126                elif ct == vtki.cell_types["PIXEL"]:
 127                    cell = vtki.vtkPixel()
 128                elif ct == vtki.cell_types["QUAD"]:
 129                    cell = vtki.vtkQuad()
 130                elif ct == vtki.cell_types["VOXEL"]:
 131                    cell = vtki.vtkVoxel()
 132                elif ct == vtki.cell_types["PYRAMID"]:
 133                    cell = vtki.vtkPyramid()
 134                elif ct == vtki.cell_types["HEXAHEDRON"]:
 135                    cell = vtki.vtkHexahedron()
 136                elif ct == vtki.cell_types["HEXAGONAL_PRISM"]:
 137                    cell = vtki.vtkHexagonalPrism()
 138                elif ct == vtki.cell_types["PENTAGONAL_PRISM"]:
 139                    cell = vtki.vtkPentagonalPrism()
 140                elif ct == vtki.cell_types["QUADRATIC_TETRA"]:
 141                    from vtkmodules.vtkCommonDataModel import vtkQuadraticTetra
 142                    cell = vtkQuadraticTetra()
 143                elif ct == vtki.cell_types["QUADRATIC_HEXAHEDRON"]:
 144                    from vtkmodules.vtkCommonDataModel import vtkQuadraticHexahedron
 145                    cell = vtkQuadraticHexahedron()
 146                elif ct == vtki.cell_types["QUADRATIC_WEDGE"]:
 147                    from vtkmodules.vtkCommonDataModel import vtkQuadraticWedge
 148                    cell = vtkQuadraticWedge()
 149                elif ct == vtki.cell_types["QUADRATIC_PYRAMID"]:
 150                    from vtkmodules.vtkCommonDataModel import vtkQuadraticPyramid
 151                    cell = vtkQuadraticPyramid()
 152                elif ct == vtki.cell_types["QUADRATIC_LINEAR_QUAD"]:
 153                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearQuad
 154                    cell = vtkQuadraticLinearQuad()
 155                elif ct == vtki.cell_types["QUADRATIC_LINEAR_WEDGE"]:
 156                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearWedge
 157                    cell = vtkQuadraticLinearWedge()
 158                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_WEDGE"]:
 159                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticWedge
 160                    cell = vtkBiQuadraticQuadraticWedge()
 161                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_HEXAHEDRON"]:
 162                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticHexahedron
 163                    cell = vtkBiQuadraticQuadraticHexahedron()
 164                elif ct == vtki.cell_types["BIQUADRATIC_TRIANGLE"]:
 165                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticTriangle
 166                    cell = vtkBiQuadraticTriangle()
 167                elif ct == vtki.cell_types["CUBIC_LINE"]:
 168                    from vtkmodules.vtkCommonDataModel import vtkCubicLine
 169                    cell = vtkCubicLine()
 170                elif ct == vtki.cell_types["CONVEX_POINT_SET"]:
 171                    from vtkmodules.vtkCommonDataModel import vtkConvexPointSet
 172                    cell = vtkConvexPointSet()
 173                elif ct == vtki.cell_types["POLYHEDRON"]:
 174                    from vtkmodules.vtkCommonDataModel import vtkPolyhedron
 175                    cell = vtkPolyhedron()
 176                elif ct == vtki.cell_types["HIGHER_ORDER_TRIANGLE"]:
 177                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTriangle
 178                    cell = vtkHigherOrderTriangle()
 179                elif ct == vtki.cell_types["HIGHER_ORDER_QUAD"]:
 180                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderQuadrilateral
 181                    cell = vtkHigherOrderQuadrilateral()
 182                elif ct == vtki.cell_types["HIGHER_ORDER_TETRAHEDRON"]:
 183                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTetra
 184                    cell = vtkHigherOrderTetra()
 185                elif ct == vtki.cell_types["HIGHER_ORDER_WEDGE"]:
 186                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderWedge
 187                    cell = vtkHigherOrderWedge()
 188                elif ct == vtki.cell_types["HIGHER_ORDER_HEXAHEDRON"]:
 189                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderHexahedron
 190                    cell = vtkHigherOrderHexahedron()
 191                elif ct == vtki.cell_types["LAGRANGE_CURVE"]:
 192                    from vtkmodules.vtkCommonDataModel import vtkLagrangeCurve
 193                    cell = vtkLagrangeCurve()
 194                elif ct == vtki.cell_types["LAGRANGE_TRIANGLE"]:
 195                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTriangle
 196                    cell = vtkLagrangeTriangle()
 197                elif ct == vtki.cell_types["LAGRANGE_QUADRILATERAL"]:
 198                    from vtkmodules.vtkCommonDataModel import vtkLagrangeQuadrilateral
 199                    cell = vtkLagrangeQuadrilateral()
 200                elif ct == vtki.cell_types["LAGRANGE_TETRAHEDRON"]:
 201                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTetra
 202                    cell = vtkLagrangeTetra()
 203                elif ct == vtki.cell_types["LAGRANGE_HEXAHEDRON"]:
 204                    from vtkmodules.vtkCommonDataModel import vtkLagrangeHexahedron
 205                    cell = vtkLagrangeHexahedron()
 206                elif ct == vtki.cell_types["LAGRANGE_WEDGE"]:
 207                    from vtkmodules.vtkCommonDataModel import vtkLagrangeWedge
 208                    cell = vtkLagrangeWedge()
 209                elif ct == vtki.cell_types["BEZIER_CURVE"]:
 210                    from vtkmodules.vtkCommonDataModel import vtkBezierCurve
 211                    cell = vtkBezierCurve()
 212                elif ct == vtki.cell_types["BEZIER_TRIANGLE"]:
 213                    from vtkmodules.vtkCommonDataModel import vtkBezierTriangle
 214                    cell = vtkBezierTriangle()
 215                elif ct == vtki.cell_types["BEZIER_QUADRILATERAL"]:
 216                    from vtkmodules.vtkCommonDataModel import vtkBezierQuadrilateral
 217                    cell = vtkBezierQuadrilateral()
 218                elif ct == vtki.cell_types["BEZIER_TETRAHEDRON"]:
 219                    from vtkmodules.vtkCommonDataModel import vtkBezierTetra
 220                    cell = vtkBezierTetra()
 221                elif ct == vtki.cell_types["BEZIER_HEXAHEDRON"]:
 222                    from vtkmodules.vtkCommonDataModel import vtkBezierHexahedron
 223                    cell = vtkBezierHexahedron()
 224                elif ct == vtki.cell_types["BEZIER_WEDGE"]:
 225                    from vtkmodules.vtkCommonDataModel import vtkBezierWedge
 226                    cell = vtkBezierWedge()
 227                else:
 228                    vedo.logger.error(
 229                        f"UnstructuredGrid: cell type {ct} not supported. Skip.")
 230                    continue
 231
 232                cpids = cell.GetPointIds()
 233                cell_conn = cells[i]
 234                for j, pid in enumerate(cell_conn):
 235                    cpids.SetId(j, pid)
 236                self.dataset.InsertNextCell(ct, cpids)
 237
 238        elif "UnstructuredGrid" in inputtype:
 239            self.dataset = inputobj
 240
 241        elif isinstance(inputobj, str):
 242            if "https://" in inputobj:
 243                inputobj = download(inputobj, verbose=False)
 244            self.filename = inputobj
 245            if inputobj.endswith(".vtu"):
 246                reader = vtki.new("XMLUnstructuredGridReader")
 247            else:
 248                reader = vtki.new("UnstructuredGridReader")
 249            self.filename = inputobj
 250            reader.SetFileName(inputobj)
 251            reader.Update()
 252            self.dataset = reader.GetOutput()
 253
 254        else:
 255            # this converts other types of vtk objects to UnstructuredGrid
 256            apf = vtki.new("AppendFilter")
 257            try:
 258                apf.AddInputData(inputobj)
 259            except TypeError:
 260                apf.AddInputData(inputobj.dataset)
 261            apf.Update()
 262            self.dataset = apf.GetOutput()
 263
 264        self.properties.SetColor(0.89, 0.455, 0.671)  # pink7
 265
 266        self.pipeline = utils.OperationNode(
 267            self, comment=f"#cells {self.dataset.GetNumberOfCells()}", c="#4cc9f0"
 268        )
 269
 270    # ------------------------------------------------------------------
 271    def __str__(self):
 272        """Print a string summary of the `UnstructuredGrid` object."""
 273        module = self.__class__.__module__
 274        name = self.__class__.__name__
 275        out = vedo.printc(
 276            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
 277            c="m", bold=True, invert=True, return_string=True,
 278        )
 279        out += "\x1b[0m\u001b[35m"
 280
 281        out += "nr. of verts".ljust(14) + ": " + str(self.npoints) + "\n"
 282        out += "nr. of cells".ljust(14) + ": " + str(self.ncells)  + "\n"
 283        ct_arr = np.unique(self.cell_types_array)
 284        cnames = [k for k, v in vtki.cell_types.items() if v in ct_arr]
 285        out += "cell types".ljust(14) + ": " + str(cnames) + "\n"
 286
 287        if self.npoints:
 288            out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6)
 289            out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n"
 290            out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n"
 291
 292        bnds = self.bounds()
 293        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
 294        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
 295        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
 296        out += "bounds".ljust(14) + ":"
 297        out += " x=(" + bx1 + ", " + bx2 + "),"
 298        out += " y=(" + by1 + ", " + by2 + "),"
 299        out += " z=(" + bz1 + ", " + bz2 + ")\n"
 300
 301        for key in self.pointdata.keys():
 302            arr = self.pointdata[key]
 303            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 304            mark_active = "pointdata"
 305            a_scalars = self.dataset.GetPointData().GetScalars()
 306            a_vectors = self.dataset.GetPointData().GetVectors()
 307            a_tensors = self.dataset.GetPointData().GetTensors()
 308            if a_scalars and a_scalars.GetName() == key:
 309                mark_active += " *"
 310            elif a_vectors and a_vectors.GetName() == key:
 311                mark_active += " **"
 312            elif a_tensors and a_tensors.GetName() == key:
 313                mark_active += " ***"
 314            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
 315            out += f", range=({rng})\n"
 316
 317        for key in self.celldata.keys():
 318            arr = self.celldata[key]
 319            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 320            mark_active = "celldata"
 321            a_scalars = self.dataset.GetCellData().GetScalars()
 322            a_vectors = self.dataset.GetCellData().GetVectors()
 323            a_tensors = self.dataset.GetCellData().GetTensors()
 324            if a_scalars and a_scalars.GetName() == key:
 325                mark_active += " *"
 326            elif a_vectors and a_vectors.GetName() == key:
 327                mark_active += " **"
 328            elif a_tensors and a_tensors.GetName() == key:
 329                mark_active += " ***"
 330            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
 331            out += f", range=({rng})\n"
 332
 333        for key in self.metadata.keys():
 334            arr = self.metadata[key]
 335            out += "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
 336
 337        return out.rstrip() + "\x1b[0m"
 338
 339    def _repr_html_(self):
 340        """
 341        HTML representation of the UnstructuredGrid object for Jupyter Notebooks.
 342
 343        Returns:
 344            HTML text with the image and some properties.
 345        """
 346        import io
 347        import base64
 348        from PIL import Image
 349
 350        library_name = "vedo.grids.UnstructuredGrid"
 351        help_url = "https://vedo.embl.es/docs/vedo/grids.html#UnstructuredGrid"
 352
 353        arr = self.thumbnail()
 354        im = Image.fromarray(arr)
 355        buffered = io.BytesIO()
 356        im.save(buffered, format="PNG", quality=100)
 357        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
 358        url = "data:image/png;base64," + encoded
 359        image = f"<img src='{url}'></img>"
 360
 361        bounds = "<br/>".join(
 362            [
 363                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
 364                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
 365            ]
 366        )
 367
 368        help_text = ""
 369        if self.name:
 370            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
 371        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
 372        if self.filename:
 373            dots = ""
 374            if len(self.filename) > 30:
 375                dots = "..."
 376            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
 377
 378        pdata = ""
 379        if self.dataset.GetPointData().GetScalars():
 380            if self.dataset.GetPointData().GetScalars().GetName():
 381                name = self.dataset.GetPointData().GetScalars().GetName()
 382                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
 383
 384        cdata = ""
 385        if self.dataset.GetCellData().GetScalars():
 386            if self.dataset.GetCellData().GetScalars().GetName():
 387                name = self.dataset.GetCellData().GetScalars().GetName()
 388                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
 389
 390        pts = self.vertices
 391        cm = np.mean(pts, axis=0)
 392
 393        all = [
 394            "<table>",
 395            "<tr>",
 396            "<td>", image, "</td>",
 397            "<td style='text-align: center; vertical-align: center;'><br/>", help_text,
 398            "<table>",
 399            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
 400            "<tr><td><b> center of mass </b></td><td>" + utils.precision(cm,3) + "</td></tr>",
 401            # "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>",
 402            "<tr><td><b> nr. points&nbsp/&nbspcells </b></td><td>"
 403            + str(self.npoints) + "&nbsp/&nbsp" + str(self.ncells) + "</td></tr>",
 404            pdata,
 405            cdata,
 406            "</table>",
 407            "</table>",
 408        ]
 409        return "\n".join(all)
 410
 411    @property
 412    def actor(self):
 413        """Return the `vtkActor` of the object."""
 414        # print("building actor")
 415        gf = vtki.new("GeometryFilter")
 416        gf.SetInputData(self.dataset)
 417        gf.Update()
 418        out = gf.GetOutput()
 419        self.mapper.SetInputData(out)
 420        self.mapper.Modified()
 421        return self._actor
 422
 423    @actor.setter
 424    def actor(self, _):
 425        pass
 426
 427    def _update(self, data, reset_locators=False):
 428        self.dataset = data
 429        if reset_locators:
 430            self.cell_locator = None
 431            self.point_locator = None
 432        return self
 433    
 434    def merge(self, *others) -> Self:
 435        """
 436        Merge multiple datasets into one single `UnstrcturedGrid`.
 437        """
 438        apf = vtki.new("AppendFilter")
 439        for o in others:
 440            if isinstance(o, UnstructuredGrid):
 441                apf.AddInputData(o.dataset)
 442            elif isinstance(o, vtki.vtkUnstructuredGrid):
 443                apf.AddInputData(o)
 444            else:
 445                vedo.printc("Error: cannot merge type", type(o), c="r")
 446        apf.Update()
 447        self._update(apf.GetOutput())
 448        self.pipeline = utils.OperationNode(
 449            "merge", parents=[self, *others], c="#9e2a2b"
 450        )
 451        return self
 452
 453    def copy(self, deep=True) -> "UnstructuredGrid":
 454        """Return a copy of the object. Alias of `clone()`."""
 455        return self.clone(deep=deep)
 456
 457    def clone(self, deep=True) -> "UnstructuredGrid":
 458        """Clone the UnstructuredGrid object to yield an exact copy."""
 459        ug = vtki.vtkUnstructuredGrid()
 460        if deep:
 461            ug.DeepCopy(self.dataset)
 462        else:
 463            ug.ShallowCopy(self.dataset)
 464        if isinstance(self, vedo.UnstructuredGrid):
 465            cloned = vedo.UnstructuredGrid(ug)
 466        else:
 467            cloned = vedo.TetMesh(ug)
 468
 469        cloned.copy_properties_from(self)
 470
 471        cloned.pipeline = utils.OperationNode(
 472            "clone", parents=[self], shape="diamond", c="#bbe1ed"
 473        )
 474        return cloned
 475
 476    def bounds(self) -> np.ndarray:
 477        """
 478        Get the object bounds.
 479        Returns a list in format `[xmin,xmax, ymin,ymax, zmin,zmax]`.
 480        """
 481        # OVERRIDE CommonAlgorithms.bounds() which is too slow
 482        return np.array(self.dataset.GetBounds())
 483
 484    def threshold(self, name=None, above=None, below=None, on="cells") -> Self:
 485        """
 486        Threshold the tetrahedral mesh by a cell scalar value.
 487        Reduce to only tets which satisfy the threshold limits.
 488
 489        - if `above = below` will only select tets with that specific value.
 490        - if `above > below` selection range is flipped.
 491
 492        Set keyword "on" to either "cells" or "points".
 493        """
 494        th = vtki.new("Threshold")
 495        th.SetInputData(self.dataset)
 496
 497        if name is None:
 498            if self.celldata.keys():
 499                name = self.celldata.keys()[0]
 500                th.SetInputArrayToProcess(0, 0, 0, 1, name)
 501            elif self.pointdata.keys():
 502                name = self.pointdata.keys()[0]
 503                th.SetInputArrayToProcess(0, 0, 0, 0, name)
 504            if name is None:
 505                vedo.logger.warning("cannot find active array. Skip.")
 506                return self
 507        else:
 508            if on.startswith("c"):
 509                th.SetInputArrayToProcess(0, 0, 0, 1, name)
 510            else:
 511                th.SetInputArrayToProcess(0, 0, 0, 0, name)
 512
 513        if above is not None:
 514            th.SetLowerThreshold(above)
 515
 516        if below is not None:
 517            th.SetUpperThreshold(below)
 518
 519        th.Update()
 520        return self._update(th.GetOutput())
 521
 522    def isosurface(self, value=None, flying_edges=False) -> "vedo.mesh.Mesh":
 523        """
 524        Return an `Mesh` isosurface extracted from the `Volume` object.
 525
 526        Set `value` as single float or list of values to draw the isosurface(s).
 527        Use flying_edges for faster results (but sometimes can interfere with `smooth()`).
 528
 529        Examples:
 530            - [isosurfaces1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/isosurfaces1.py)
 531
 532                ![](https://vedo.embl.es/images/volumetric/isosurfaces.png)
 533        """
 534        scrange = self.dataset.GetScalarRange()
 535
 536        if flying_edges:
 537            cf = vtki.new("FlyingEdges3D")
 538            cf.InterpolateAttributesOn()
 539        else:
 540            cf = vtki.new("ContourFilter")
 541            cf.UseScalarTreeOn()
 542
 543        cf.SetInputData(self.dataset)
 544        cf.ComputeNormalsOn()
 545
 546        if utils.is_sequence(value):
 547            cf.SetNumberOfContours(len(value))
 548            for i, t in enumerate(value):
 549                cf.SetValue(i, t)
 550        else:
 551            if value is None:
 552                value = (2 * scrange[0] + scrange[1]) / 3.0
 553                # print("automatic isosurface value =", value)
 554            cf.SetValue(0, value)
 555
 556        cf.Update()
 557        poly = cf.GetOutput()
 558
 559        out = vedo.mesh.Mesh(poly, c=None).flat()
 560        out.mapper.SetScalarRange(scrange[0], scrange[1])
 561
 562        out.pipeline = utils.OperationNode(
 563            "isosurface",
 564            parents=[self],
 565            comment=f"#pts {out.dataset.GetNumberOfPoints()}",
 566            c="#4cc9f0:#e9c46a",
 567        )
 568        return out
 569
 570    def shrink(self, fraction=0.8) -> Self:
 571        """
 572        Shrink the individual cells.
 573
 574        ![](https://vedo.embl.es/images/feats/shrink_hex.png)
 575        """
 576        sf = vtki.new("ShrinkFilter")
 577        sf.SetInputData(self.dataset)
 578        sf.SetShrinkFactor(fraction)
 579        sf.Update()
 580        out = sf.GetOutput()
 581        self._update(out)
 582        self.pipeline = utils.OperationNode(
 583            "shrink", comment=f"by {fraction}", parents=[self], c="#9e2a2b"
 584        )
 585        return self
 586
 587    def tomesh(self, fill=False, shrink=1.0) -> "vedo.mesh.Mesh":
 588        """
 589        Build a polygonal `Mesh` from the current object.
 590
 591        If `fill=True`, the interior faces of all the cells are created.
 592        (setting a `shrink` value slightly smaller than the default 1.0
 593        can avoid flickering due to internal adjacent faces).
 594
 595        If `fill=False`, only the boundary faces will be generated.
 596        """
 597        gf = vtki.new("GeometryFilter")
 598        if fill:
 599            sf = vtki.new("ShrinkFilter")
 600            sf.SetInputData(self.dataset)
 601            sf.SetShrinkFactor(shrink)
 602            sf.Update()
 603            gf.SetInputData(sf.GetOutput())
 604            gf.Update()
 605            poly = gf.GetOutput()
 606        else:
 607            gf.SetInputData(self.dataset)
 608            gf.Update()
 609            poly = gf.GetOutput()
 610
 611        msh = vedo.mesh.Mesh(poly)
 612        msh.copy_properties_from(self)
 613        msh.pipeline = utils.OperationNode(
 614            "tomesh", parents=[self], comment=f"fill={fill}", c="#9e2a2b:#e9c46a"
 615        )
 616        return msh
 617
 618    @property
 619    def cell_types_array(self):
 620        """Return the list of cell types in the dataset."""
 621        uarr = self.dataset.GetCellTypesArray()
 622        return utils.vtk2numpy(uarr)
 623
 624    def extract_cells_by_type(self, ctype) -> "UnstructuredGrid":
 625        """Extract a specific cell type and return a new `UnstructuredGrid`."""
 626        if isinstance(ctype, str):
 627            try:
 628                ctype = vtki.cell_types[ctype.upper()]
 629            except KeyError:
 630                vedo.logger.error(f"extract_cells_by_type: cell type {ctype} does not exist. Skip.")
 631                return self
 632        uarr = self.dataset.GetCellTypesArray()
 633        ctarrtyp = np.where(utils.vtk2numpy(uarr) == ctype)[0]
 634        uarrtyp = utils.numpy2vtk(ctarrtyp, deep=False, dtype="id")
 635        selection_node = vtki.new("SelectionNode")
 636        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
 637        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
 638        selection_node.SetSelectionList(uarrtyp)
 639        selection = vtki.new("Selection")
 640        selection.AddNode(selection_node)
 641        es = vtki.new("ExtractSelection")
 642        es.SetInputData(0, self.dataset)
 643        es.SetInputData(1, selection)
 644        es.Update()
 645
 646        ug = UnstructuredGrid(es.GetOutput())
 647        ug.pipeline = utils.OperationNode(
 648            "extract_cell_type", comment=f"type {ctype}", c="#edabab", parents=[self]
 649        )
 650        return ug
 651
 652    def extract_cells_by_id(self, idlist, use_point_ids=False) -> "UnstructuredGrid":
 653        """Return a new `UnstructuredGrid` composed of the specified subset of indices."""
 654        selection_node = vtki.new("SelectionNode")
 655        if use_point_ids:
 656            selection_node.SetFieldType(vtki.get_class("SelectionNode").POINT)
 657            contcells = vtki.get_class("SelectionNode").CONTAINING_CELLS()
 658            selection_node.GetProperties().Set(contcells, 1)
 659        else:
 660            selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
 661        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
 662        vidlist = utils.numpy2vtk(idlist, dtype="id")
 663        selection_node.SetSelectionList(vidlist)
 664        selection = vtki.new("Selection")
 665        selection.AddNode(selection_node)
 666        es = vtki.new("ExtractSelection")
 667        es.SetInputData(0, self)
 668        es.SetInputData(1, selection)
 669        es.Update()
 670
 671        ug = UnstructuredGrid(es.GetOutput())
 672        pr = vtki.vtkProperty()
 673        pr.DeepCopy(self.properties)
 674        ug.actor.SetProperty(pr)
 675        ug.properties = pr
 676
 677        ug.mapper.SetLookupTable(utils.ctf2lut(self))
 678        ug.pipeline = utils.OperationNode(
 679            "extract_cells_by_id",
 680            parents=[self],
 681            comment=f"#cells {self.dataset.GetNumberOfCells()}",
 682            c="#9e2a2b",
 683        )
 684        return ug
 685
 686    def find_cell(self, p: list) -> int:
 687        """Locate the cell that contains a point and return the cell ID."""
 688        if self.cell_locator is None:
 689            self.cell_locator = vtki.new("CellLocator")
 690            self.cell_locator.SetDataSet(self.dataset)
 691            self.cell_locator.BuildLocator()
 692        cid = self.cell_locator.FindCell(p)
 693        return cid
 694
 695    def clean(self) -> Self:
 696        """
 697        Cleanup unused points and empty cells
 698        """
 699        cl = vtki.new("StaticCleanUnstructuredGrid")
 700        cl.SetInputData(self.dataset)
 701        cl.RemoveUnusedPointsOn()
 702        cl.ProduceMergeMapOff()
 703        cl.AveragePointDataOff()
 704        cl.Update()
 705
 706        self._update(cl.GetOutput())
 707        self.pipeline = utils.OperationNode(
 708            "clean",
 709            parents=[self],
 710            comment=f"#cells {self.dataset.GetNumberOfCells()}",
 711            c="#9e2a2b",
 712        )
 713        return self
 714
 715    def extract_cells_on_plane(self, origin: tuple, normal: tuple) -> Self:
 716        """
 717        Extract cells that are lying of the specified surface.
 718        """
 719        bf = vtki.new("3DLinearGridCrinkleExtractor")
 720        bf.SetInputData(self.dataset)
 721        bf.CopyPointDataOn()
 722        bf.CopyCellDataOn()
 723        bf.RemoveUnusedPointsOff()
 724
 725        plane = vtki.new("Plane")
 726        plane.SetOrigin(origin)
 727        plane.SetNormal(normal)
 728        bf.SetImplicitFunction(plane)
 729        bf.Update()
 730
 731        self._update(bf.GetOutput(), reset_locators=False)
 732        self.pipeline = utils.OperationNode(
 733            "extract_cells_on_plane",
 734            parents=[self],
 735            comment=f"#cells {self.dataset.GetNumberOfCells()}",
 736            c="#9e2a2b",
 737        )
 738        return self
 739
 740    def extract_cells_on_sphere(self, center: tuple, radius: tuple) -> Self:
 741        """
 742        Extract cells that are lying of the specified surface.
 743        """
 744        bf = vtki.new("3DLinearGridCrinkleExtractor")
 745        bf.SetInputData(self.dataset)
 746        bf.CopyPointDataOn()
 747        bf.CopyCellDataOn()
 748        bf.RemoveUnusedPointsOff()
 749
 750        sph = vtki.new("Sphere")
 751        sph.SetRadius(radius)
 752        sph.SetCenter(center)
 753        bf.SetImplicitFunction(sph)
 754        bf.Update()
 755
 756        self._update(bf.GetOutput())
 757        self.pipeline = utils.OperationNode(
 758            "extract_cells_on_sphere",
 759            parents=[self],
 760            comment=f"#cells {self.dataset.GetNumberOfCells()}",
 761            c="#9e2a2b",
 762        )
 763        return self
 764
 765    def extract_cells_on_cylinder(self, center: tuple, axis: tuple, radius: float) -> Self:
 766        """
 767        Extract cells that are lying of the specified surface.
 768        """
 769        bf = vtki.new("3DLinearGridCrinkleExtractor")
 770        bf.SetInputData(self.dataset)
 771        bf.CopyPointDataOn()
 772        bf.CopyCellDataOn()
 773        bf.RemoveUnusedPointsOff()
 774
 775        cyl = vtki.new("Cylinder")
 776        cyl.SetRadius(radius)
 777        cyl.SetCenter(center)
 778        cyl.SetAxis(axis)
 779        bf.SetImplicitFunction(cyl)
 780        bf.Update()
 781
 782        self.pipeline = utils.OperationNode(
 783            "extract_cells_on_cylinder",
 784            parents=[self],
 785            comment=f"#cells {self.dataset.GetNumberOfCells()}",
 786            c="#9e2a2b",
 787        )
 788        self._update(bf.GetOutput())
 789        return self
 790
 791    def cut_with_plane(self, origin=(0, 0, 0), normal="x") -> "UnstructuredGrid":
 792        """
 793        Cut the object with the plane defined by a point and a normal.
 794
 795        Arguments:
 796            origin : (list)
 797                the cutting plane goes through this point
 798            normal : (list, str)
 799                normal vector to the cutting plane
 800        """
 801        # if isinstance(self, vedo.Volume):
 802        #     raise RuntimeError("cut_with_plane() is not applicable to Volume objects.")
 803
 804        strn = str(normal)
 805        if strn   ==  "x": normal = (1, 0, 0)
 806        elif strn ==  "y": normal = (0, 1, 0)
 807        elif strn ==  "z": normal = (0, 0, 1)
 808        elif strn == "-x": normal = (-1, 0, 0)
 809        elif strn == "-y": normal = (0, -1, 0)
 810        elif strn == "-z": normal = (0, 0, -1)
 811        plane = vtki.new("Plane")
 812        plane.SetOrigin(origin)
 813        plane.SetNormal(normal)
 814        clipper = vtki.new("ClipDataSet")
 815        clipper.SetInputData(self.dataset)
 816        clipper.SetClipFunction(plane)
 817        clipper.GenerateClipScalarsOff()
 818        clipper.GenerateClippedOutputOff()
 819        clipper.SetValue(0)
 820        clipper.Update()
 821        cout = clipper.GetOutput()
 822
 823        if isinstance(cout, vtki.vtkUnstructuredGrid):
 824            ug = vedo.UnstructuredGrid(cout)
 825            if isinstance(self, vedo.UnstructuredGrid):
 826                self._update(cout)
 827                self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
 828                return self
 829            ug.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
 830            return ug
 831
 832        else:
 833            self._update(cout)
 834            self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
 835            return self
 836
 837    def cut_with_box(self, box: Any) -> "UnstructuredGrid":
 838        """
 839        Cut the grid with the specified bounding box.
 840
 841        Parameter box has format [xmin, xmax, ymin, ymax, zmin, zmax].
 842        If an object is passed, its bounding box are used.
 843
 844        This method always returns a TetMesh object.
 845
 846        Example:
 847        ```python
 848        from vedo import *
 849        tmesh = TetMesh(dataurl+'limb_ugrid.vtk')
 850        tmesh.color('rainbow')
 851        cu = Cube(side=500).x(500) # any Mesh works
 852        tmesh.cut_with_box(cu).show(axes=1)
 853        ```
 854
 855        ![](https://vedo.embl.es/images/feats/tet_cut_box.png)
 856        """
 857        bc = vtki.new("BoxClipDataSet")
 858        bc.SetInputData(self.dataset)
 859        try:
 860            boxb = box.bounds()
 861        except AttributeError:
 862            boxb = box
 863
 864        bc.SetBoxClip(*boxb)
 865        bc.Update()
 866        cout = bc.GetOutput()
 867
 868        # output of vtkBoxClipDataSet is always tetrahedrons
 869        tm = vedo.TetMesh(cout)
 870        tm.pipeline = utils.OperationNode("cut_with_box", parents=[self], c="#9e2a2b")
 871        return tm
 872
 873    def cut_with_mesh(self, mesh: Mesh, invert=False, whole_cells=False, on_boundary=False) -> "UnstructuredGrid":
 874        """
 875        Cut a `UnstructuredGrid` or `TetMesh` with a `Mesh`.
 876
 877        Use `invert` to return cut off part of the input object.
 878        """
 879        ug = self.dataset
 880
 881        ippd = vtki.new("ImplicitPolyDataDistance")
 882        ippd.SetInput(mesh.dataset)
 883
 884        if whole_cells or on_boundary:
 885            clipper = vtki.new("ExtractGeometry")
 886            clipper.SetInputData(ug)
 887            clipper.SetImplicitFunction(ippd)
 888            clipper.SetExtractInside(not invert)
 889            clipper.SetExtractBoundaryCells(False)
 890            if on_boundary:
 891                clipper.SetExtractBoundaryCells(True)
 892                clipper.SetExtractOnlyBoundaryCells(True)
 893        else:
 894            signed_dists = vtki.vtkFloatArray()
 895            signed_dists.SetNumberOfComponents(1)
 896            signed_dists.SetName("SignedDistance")
 897            for pointId in range(ug.GetNumberOfPoints()):
 898                p = ug.GetPoint(pointId)
 899                signed_dist = ippd.EvaluateFunction(p)
 900                signed_dists.InsertNextValue(signed_dist)
 901            ug.GetPointData().AddArray(signed_dists)
 902            ug.GetPointData().SetActiveScalars("SignedDistance")  # NEEDED
 903            clipper = vtki.new("ClipDataSet")
 904            clipper.SetInputData(ug)
 905            clipper.SetInsideOut(not invert)
 906            clipper.SetValue(0.0)
 907
 908        clipper.Update()
 909
 910        out = vedo.UnstructuredGrid(clipper.GetOutput())
 911        out.pipeline = utils.OperationNode("cut_with_mesh", parents=[self], c="#9e2a2b")
 912        return out
 913
 914
 915##########################################################################
 916class TetMesh(UnstructuredGrid):
 917    """The class describing tetrahedral meshes."""
 918
 919    def __init__(self, inputobj=None):
 920        """
 921        Arguments:
 922            inputobj : (vtkUnstructuredGrid, list, str, tetgenpy.TetgenIO)
 923                list of points and tet indices, or filename
 924        """
 925        super().__init__()
 926
 927        self.dataset = None
 928
 929        self.mapper = vtki.new("PolyDataMapper")
 930        self._actor = vtki.vtkActor()
 931        self._actor.retrieve_object = weak_ref_to(self)
 932        self._actor.SetMapper(self.mapper)
 933        self.properties = self._actor.GetProperty()
 934
 935        self.name = "TetMesh"
 936
 937        # print('TetMesh inputtype', type(inputobj))
 938
 939        ###################
 940        if inputobj is None:
 941            self.dataset = vtki.vtkUnstructuredGrid()
 942
 943        elif isinstance(inputobj, vtki.vtkUnstructuredGrid):
 944            self.dataset = inputobj
 945
 946        elif isinstance(inputobj, UnstructuredGrid):
 947            self.dataset = inputobj.dataset
 948
 949        elif "TetgenIO" in str(type(inputobj)):  # tetgenpy object
 950            inputobj = [inputobj.points(), inputobj.tetrahedra()]
 951
 952        elif isinstance(inputobj, vtki.vtkRectilinearGrid):
 953            r2t = vtki.new("RectilinearGridToTetrahedra")
 954            r2t.SetInputData(inputobj)
 955            r2t.RememberVoxelIdOn()
 956            r2t.SetTetraPerCellTo6()
 957            r2t.Update()
 958            self.dataset = r2t.GetOutput()
 959
 960        elif isinstance(inputobj, vtki.vtkDataSet):
 961            r2t = vtki.new("DataSetTriangleFilter")
 962            r2t.SetInputData(inputobj)
 963            r2t.TetrahedraOnlyOn()
 964            r2t.Update()
 965            self.dataset = r2t.GetOutput()
 966
 967        elif isinstance(inputobj, str):
 968            if "https://" in inputobj:
 969                inputobj = download(inputobj, verbose=False)
 970            if inputobj.endswith(".vtu"):
 971                reader = vtki.new("XMLUnstructuredGridReader")
 972            else:
 973                reader = vtki.new("UnstructuredGridReader")
 974
 975            if not os.path.isfile(inputobj):
 976                # for some reason vtk Reader does not complain
 977                vedo.logger.error(f"file {inputobj} not found")
 978                raise FileNotFoundError
 979
 980            self.filename = inputobj
 981            reader.SetFileName(inputobj)
 982            reader.Update()
 983            ug = reader.GetOutput()
 984
 985            tt = vtki.new("DataSetTriangleFilter")
 986            tt.SetInputData(ug)
 987            tt.SetTetrahedraOnly(True)
 988            tt.Update()
 989            self.dataset = tt.GetOutput()
 990
 991        ###############################
 992        if utils.is_sequence(inputobj):
 993            self.dataset = vtki.vtkUnstructuredGrid()
 994
 995            points, cells = inputobj
 996            if len(points) == 0:
 997                return
 998            if not utils.is_sequence(points[0]):
 999                return
1000            if len(cells) == 0:
1001                return
1002
1003            if not utils.is_sequence(cells[0]):
1004                tets = []
1005                nf = cells[0] + 1
1006                for i, cl in enumerate(cells):
1007                    if i in (nf, 0):
1008                        k = i + 1
1009                        nf = cl + k
1010                        cell = [cells[j + k] for j in range(cl)]
1011                        tets.append(cell)
1012                cells = tets
1013
1014            source_points = vtki.vtkPoints()
1015            varr = utils.numpy2vtk(points, dtype=np.float32)
1016            source_points.SetData(varr)
1017            self.dataset.SetPoints(source_points)
1018
1019            source_tets = vtki.vtkCellArray()
1020            for f in cells:
1021                ele = vtki.vtkTetra()
1022                pid = ele.GetPointIds()
1023                for i, fi in enumerate(f):
1024                    pid.SetId(i, fi)
1025                source_tets.InsertNextCell(ele)
1026            self.dataset.SetCells(vtki.cell_types["TETRA"], source_tets)
1027
1028        if not self.dataset:
1029            vedo.logger.error(f"cannot understand input type {type(inputobj)}")
1030            return
1031
1032        self.properties.SetColor(0.352, 0.612, 0.996)  # blue7
1033
1034        self.pipeline = utils.OperationNode(
1035            self, comment=f"#tets {self.dataset.GetNumberOfCells()}", c="#9e2a2b"
1036        )
1037
1038    ##################################################################
1039    def __str__(self):
1040        """Print a string summary of the `TetMesh` object."""
1041        module = self.__class__.__module__
1042        name = self.__class__.__name__
1043        out = vedo.printc(
1044            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
1045            c="c", bold=True, invert=True, return_string=True,
1046        )
1047        out += "\x1b[0m\u001b[36m"
1048
1049        out += "nr. of verts".ljust(14) + ": " + str(self.npoints) + "\n"
1050        out += "nr. of tetras".ljust(14) + ": " + str(self.ncells) + "\n"
1051
1052        if self.npoints:
1053            out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6)
1054            out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n"
1055            out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n"
1056
1057        bnds = self.bounds()
1058        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
1059        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
1060        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
1061        out += "bounds".ljust(14) + ":"
1062        out += " x=(" + bx1 + ", " + bx2 + "),"
1063        out += " y=(" + by1 + ", " + by2 + "),"
1064        out += " z=(" + bz1 + ", " + bz2 + ")\n"
1065
1066        for key in self.pointdata.keys():
1067            arr = self.pointdata[key]
1068            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
1069            mark_active = "pointdata"
1070            a_scalars = self.dataset.GetPointData().GetScalars()
1071            a_vectors = self.dataset.GetPointData().GetVectors()
1072            a_tensors = self.dataset.GetPointData().GetTensors()
1073            if a_scalars and a_scalars.GetName() == key:
1074                mark_active += " *"
1075            elif a_vectors and a_vectors.GetName() == key:
1076                mark_active += " **"
1077            elif a_tensors and a_tensors.GetName() == key:
1078                mark_active += " ***"
1079            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
1080            out += f", range=({rng})\n"
1081
1082        for key in self.celldata.keys():
1083            arr = self.celldata[key]
1084            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
1085            mark_active = "celldata"
1086            a_scalars = self.dataset.GetCellData().GetScalars()
1087            a_vectors = self.dataset.GetCellData().GetVectors()
1088            a_tensors = self.dataset.GetCellData().GetTensors()
1089            if a_scalars and a_scalars.GetName() == key:
1090                mark_active += " *"
1091            elif a_vectors and a_vectors.GetName() == key:
1092                mark_active += " **"
1093            elif a_tensors and a_tensors.GetName() == key:
1094                mark_active += " ***"
1095            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
1096            out += f", range=({rng})\n"
1097
1098        for key in self.metadata.keys():
1099            arr = self.metadata[key]
1100            out += "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
1101
1102        return out.rstrip() + "\x1b[0m"
1103
1104    def _repr_html_(self):
1105        """
1106        HTML representation of the TetMesh object for Jupyter Notebooks.
1107
1108        Returns:
1109            HTML text with the image and some properties.
1110        """
1111        import io
1112        import base64
1113        from PIL import Image
1114
1115        library_name = "vedo.grids.TetMesh"
1116        help_url = "https://vedo.embl.es/docs/vedo/grids.html#TetMesh"
1117
1118        arr = self.thumbnail()
1119        im = Image.fromarray(arr)
1120        buffered = io.BytesIO()
1121        im.save(buffered, format="PNG", quality=100)
1122        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
1123        url = "data:image/png;base64," + encoded
1124        image = f"<img src='{url}'></img>"
1125
1126        bounds = "<br/>".join(
1127            [
1128                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
1129                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
1130            ]
1131        )
1132
1133        help_text = ""
1134        if self.name:
1135            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
1136        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
1137        if self.filename:
1138            dots = ""
1139            if len(self.filename) > 30:
1140                dots = "..."
1141            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
1142
1143        pdata = ""
1144        if self.dataset.GetPointData().GetScalars():
1145            if self.dataset.GetPointData().GetScalars().GetName():
1146                name = self.dataset.GetPointData().GetScalars().GetName()
1147                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
1148
1149        cdata = ""
1150        if self.dataset.GetCellData().GetScalars():
1151            if self.dataset.GetCellData().GetScalars().GetName():
1152                name = self.dataset.GetCellData().GetScalars().GetName()
1153                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
1154
1155        pts = self.vertices
1156        cm = np.mean(pts, axis=0)
1157
1158        allt = [
1159            "<table>",
1160            "<tr>",
1161            "<td>", image, "</td>",
1162            "<td style='text-align: center; vertical-align: center;'><br/>", help_text,
1163            "<table>",
1164            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
1165            "<tr><td><b> center of mass </b></td><td>" + utils.precision(cm,3) + "</td></tr>",
1166            "<tr><td><b> nr. points&nbsp/&nbsptets </b></td><td>"
1167            + str(self.npoints) + "&nbsp/&nbsp" + str(self.ncells) + "</td></tr>",
1168            pdata,
1169            cdata,
1170            "</table>",
1171            "</table>",
1172        ]
1173        return "\n".join(allt)
1174
1175    def compute_quality(self, metric=7) -> np.ndarray:
1176        """
1177        Calculate functions of quality for the elements of a tetrahedral mesh.
1178        This method adds to the mesh a cell array named "Quality".
1179
1180        Arguments:
1181            metric : (int)
1182                type of estimators:
1183                    - EDGE RATIO, 0
1184                    - ASPECT RATIO, 1
1185                    - RADIUS RATIO, 2
1186                    - ASPECT FROBENIUS, 3
1187                    - MIN_ANGLE, 4
1188                    - COLLAPSE RATIO, 5
1189                    - ASPECT GAMMA, 6
1190                    - VOLUME, 7
1191                    - ...
1192
1193        See class [vtkMeshQuality](https://vtk.org/doc/nightly/html/classvtkMeshQuality.html)
1194        for an explanation of the meaning of each metric..
1195        """
1196        qf = vtki.new("MeshQuality")
1197        qf.SetInputData(self.dataset)
1198        qf.SetTetQualityMeasure(metric)
1199        qf.SaveCellQualityOn()
1200        qf.Update()
1201        self._update(qf.GetOutput())
1202        return utils.vtk2numpy(qf.GetOutput().GetCellData().GetArray("Quality"))
1203
1204    def check_validity(self, tol=0) -> np.ndarray:
1205        """
1206        Return an array of possible problematic tets following this convention:
1207        ```python
1208        Valid               =  0
1209        WrongNumberOfPoints = 01
1210        IntersectingEdges   = 02
1211        IntersectingFaces   = 04
1212        NoncontiguousEdges  = 08
1213        Nonconvex           = 10
1214        OrientedIncorrectly = 20
1215        ```
1216
1217        Arguments:
1218            tol : (float)
1219                This value is used as an epsilon for floating point
1220                equality checks throughout the cell checking process.
1221        """
1222        vald = vtki.new("CellValidator")
1223        if tol:
1224            vald.SetTolerance(tol)
1225        vald.SetInputData(self.dataset)
1226        vald.Update()
1227        varr = vald.GetOutput().GetCellData().GetArray("ValidityState")
1228        return utils.vtk2numpy(varr)
1229
1230    def decimate(self, scalars_name: str, fraction=0.5, n=0) -> Self:
1231        """
1232        Downsample the number of tets in a TetMesh to a specified fraction.
1233        Either `fraction` or `n` must be set.
1234
1235        Arguments:
1236            fraction : (float)
1237                the desired final fraction of the total.
1238            n : (int)
1239                the desired number of final tets
1240
1241        .. note:: setting `fraction=0.1` leaves 10% of the original nr of tets.
1242        """
1243        decimate = vtki.new("UnstructuredGridQuadricDecimation")
1244        decimate.SetInputData(self.dataset)
1245        decimate.SetScalarsName(scalars_name)
1246
1247        if n:  # n = desired number of points
1248            decimate.SetNumberOfTetsOutput(n)
1249        else:
1250            decimate.SetTargetReduction(1 - fraction)
1251        decimate.Update()
1252        self._update(decimate.GetOutput())
1253        self.pipeline = utils.OperationNode(
1254            "decimate", comment=f"array: {scalars_name}", c="#edabab", parents=[self]
1255        )
1256        return self
1257
1258    def subdivide(self) -> Self:
1259        """
1260        Increase the number of tetrahedrons of a `TetMesh`.
1261        Subdivides each tetrahedron into twelve smaller tetras.
1262        """
1263        sd = vtki.new("SubdivideTetra")
1264        sd.SetInputData(self.dataset)
1265        sd.Update()
1266        self._update(sd.GetOutput())
1267        self.pipeline = utils.OperationNode("subdivide", c="#edabab", parents=[self])
1268        return self
1269
1270    def generate_random_points(self, n, min_radius=0) -> "vedo.Points":
1271        """
1272        Generate `n` uniformly distributed random points
1273        inside the tetrahedral mesh.
1274
1275        A new point data array is added to the output points
1276        called "OriginalCellID" which contains the index of
1277        the cell ID in which the point was generated.
1278
1279        Arguments:
1280            n : (int)
1281                number of points to generate.
1282            min_radius: (float)
1283                impose a minimum distance between points.
1284                If `min_radius` is set to 0, the points are
1285                generated uniformly at random inside the mesh.
1286                If `min_radius` is set to a positive value,
1287                the points are generated uniformly at random
1288                inside the mesh, but points closer than `min_radius`
1289                to any other point are discarded.
1290
1291        Returns a `vedo.Points` object.
1292
1293        Note:
1294            Consider using `points.probe(msh)` to interpolate
1295            any existing mesh data onto the points.
1296
1297        Example:
1298        ```python
1299        from vedo import *
1300        tmesh = TetMesh(dataurl + "limb.vtu").alpha(0.2)
1301        pts = tmesh.generate_random_points(20000, min_radius=10)
1302        print(pts.pointdata["OriginalCellID"])
1303        show(pts, tmesh, axes=1).close()
1304        ```
1305        """
1306        cmesh = self.compute_cell_size()
1307        tets = cmesh.cells
1308        verts = cmesh.vertices
1309        cumul = np.cumsum(np.abs(cmesh.celldata["Volume"]))
1310
1311        out_pts = []
1312        orig_cell = []
1313        for _ in range(n):
1314            random_area = np.random.random() * cumul[-1]
1315            it = np.searchsorted(cumul, random_area)
1316            A, B, C, D = verts[tets[it]]
1317            r1, r2, r3 = sorted(np.random.random(3))
1318            p = r1 * A + (r2 - r1) * B + (r3 - r2) * C + (1 - r3) * D
1319            out_pts.append(p)
1320            orig_cell.append(it)
1321        orig_cellnp = np.array(orig_cell, dtype=np.uint32)
1322
1323        vpts = vedo.pointcloud.Points(out_pts)
1324        vpts.pointdata["OriginalCellID"] = orig_cellnp
1325
1326        if min_radius > 0:
1327            vpts.subsample(min_radius, absolute=True)
1328
1329        vpts.point_size(5).color("k1")
1330        vpts.name = "RandomPoints"
1331        vpts.pipeline = utils.OperationNode(
1332            "generate_random_points", c="#edabab", parents=[self])
1333        return vpts
1334
1335    def isosurface(self, value=True, flying_edges=None) -> "vedo.Mesh":
1336        """
1337        Return a `vedo.Mesh` isosurface.
1338        The "isosurface" is the surface of the region of points
1339        whose values equal to `value`.
1340
1341        Set `value` to a single value or list of values to compute the isosurface(s).
1342
1343        Note that flying_edges option is not available for `TetMesh`.
1344        """
1345        if flying_edges is not None:
1346            vedo.logger.warning("flying_edges option is not available for TetMesh.")
1347
1348        if not self.dataset.GetPointData().GetScalars():
1349            vedo.logger.warning(
1350                "in isosurface() no scalar pointdata found. "
1351                "Mappping cells to points."
1352            )
1353            self.map_cells_to_points()
1354        scrange = self.dataset.GetPointData().GetScalars().GetRange()
1355        cf = vtki.new("ContourFilter")  # vtki.new("ContourGrid")
1356        cf.SetInputData(self.dataset)
1357
1358        if utils.is_sequence(value):
1359            cf.SetNumberOfContours(len(value))
1360            for i, t in enumerate(value):
1361                cf.SetValue(i, t)
1362            cf.Update()
1363        else:
1364            if value is True:
1365                value = (2 * scrange[0] + scrange[1]) / 3.0
1366            cf.SetValue(0, value)
1367            cf.Update()
1368
1369        msh = Mesh(cf.GetOutput(), c=None)
1370        msh.copy_properties_from(self)
1371        msh.pipeline = utils.OperationNode("isosurface", c="#edabab", parents=[self])
1372        return msh
1373
1374    def slice(self, origin=(0, 0, 0), normal=(1, 0, 0)) -> "vedo.Mesh":
1375        """
1376        Return a 2D slice of the mesh by a plane passing through origin and assigned normal.
1377        """
1378        strn = str(normal)
1379        if   strn ==  "x": normal = (1, 0, 0)
1380        elif strn ==  "y": normal = (0, 1, 0)
1381        elif strn ==  "z": normal = (0, 0, 1)
1382        elif strn == "-x": normal = (-1, 0, 0)
1383        elif strn == "-y": normal = (0, -1, 0)
1384        elif strn == "-z": normal = (0, 0, -1)
1385        plane = vtki.new("Plane")
1386        plane.SetOrigin(origin)
1387        plane.SetNormal(normal)
1388
1389        cc = vtki.new("Cutter")
1390        cc.SetInputData(self.dataset)
1391        cc.SetCutFunction(plane)
1392        cc.Update()
1393        msh = Mesh(cc.GetOutput()).flat().lighting("ambient")
1394        msh.copy_properties_from(self)
1395        msh.pipeline = utils.OperationNode("slice", c="#edabab", parents=[self])
1396        return msh
1397
1398
1399##########################################################################
1400class RectilinearGrid(PointAlgorithms, MeshVisual):
1401    """
1402    Build a rectilinear grid.
1403    """
1404
1405    def __init__(self, inputobj=None):
1406        """
1407        A RectilinearGrid is a dataset where edges are parallel to the coordinate axes.
1408        It can be thought of as a tessellation of a box in 3D space, similar to a `Volume`
1409        except that the cells are not necessarily cubes, but they can have different lengths
1410        along each axis.
1411        This can be useful to describe a volume with variable resolution where one needs
1412        to represent a region with higher detail with respect to another region.
1413
1414        Arguments:
1415            inputobj : (vtkRectilinearGrid, list, str)
1416                list of points and tet indices, or filename
1417        
1418        Example:
1419            ```python
1420            from vedo import RectilinearGrid, show
1421
1422            xcoords = 7 + np.sqrt(np.arange(0,2500,25))
1423            ycoords = np.arange(0, 20)
1424            zcoords = np.arange(0, 20)
1425
1426            rgrid = RectilinearGrid([xcoords, ycoords, zcoords])
1427
1428            print(rgrid)
1429            print(rgrid.x_coordinates().shape)
1430            print(rgrid.compute_structured_coords([20,10,11]))
1431
1432            msh = rgrid.tomesh().lw(1)
1433
1434            show(msh, axes=1, viewup="z")
1435            ```
1436        """
1437
1438        super().__init__()
1439
1440        self.dataset = None
1441
1442        self.mapper = vtki.new("PolyDataMapper")
1443        self._actor = vtki.vtkActor()
1444        self._actor.retrieve_object = weak_ref_to(self)
1445        self._actor.SetMapper(self.mapper)
1446        self.properties = self._actor.GetProperty()
1447
1448        self.transform = LinearTransform()
1449        self.point_locator = None
1450        self.cell_locator = None
1451        self.line_locator = None
1452
1453        self.name = "RectilinearGrid"
1454        self.filename = ""
1455
1456        self.info = {}
1457        self.time =  time.time()
1458
1459        ###############################
1460        if inputobj is None:
1461            self.dataset = vtki.vtkRectilinearGrid()
1462
1463        elif isinstance(inputobj, vtki.vtkRectilinearGrid):
1464            self.dataset = inputobj
1465
1466        elif isinstance(inputobj, RectilinearGrid):
1467            self.dataset = inputobj.dataset
1468
1469        elif isinstance(inputobj, str):
1470            if "https://" in inputobj:
1471                inputobj = download(inputobj, verbose=False)
1472            if inputobj.endswith(".vtr"):
1473                reader = vtki.new("XMLRectilinearGridReader")
1474            else:
1475                reader = vtki.new("RectilinearGridReader")
1476            self.filename = inputobj
1477            reader.SetFileName(inputobj)
1478            reader.Update()
1479            self.dataset = reader.GetOutput()
1480        
1481        elif utils.is_sequence(inputobj):
1482            self.dataset = vtki.vtkRectilinearGrid()
1483            xcoords, ycoords, zcoords = inputobj
1484            nx, ny, nz = len(xcoords), len(ycoords), len(zcoords)
1485            self.dataset.SetDimensions(nx, ny, nz)
1486            self.dataset.SetXCoordinates(utils.numpy2vtk(xcoords))
1487            self.dataset.SetYCoordinates(utils.numpy2vtk(ycoords))
1488            self.dataset.SetZCoordinates(utils.numpy2vtk(zcoords))
1489
1490        ###############################
1491
1492        if not self.dataset:
1493            vedo.logger.error(f"RectilinearGrid: cannot understand input type {type(inputobj)}")
1494            return
1495
1496        self.properties.SetColor(0.352, 0.612, 0.996)  # blue7
1497
1498        self.pipeline = utils.OperationNode(
1499            self, comment=f"#tets {self.dataset.GetNumberOfCells()}", c="#9e2a2b"
1500        )
1501
1502    @property
1503    def actor(self):
1504        """Return the `vtkActor` of the object."""
1505        gf = vtki.new("GeometryFilter")
1506        gf.SetInputData(self.dataset)
1507        gf.Update()
1508        self.mapper.SetInputData(gf.GetOutput())
1509        self.mapper.Modified()
1510        return self._actor
1511
1512    @actor.setter
1513    def actor(self, _):
1514        pass
1515
1516    def _update(self, data, reset_locators=False):
1517        self.dataset = data
1518        if reset_locators:
1519            self.cell_locator = None
1520            self.point_locator = None
1521        return self
1522
1523    ##################################################################
1524    def __str__(self):
1525        """Print a summary for the `RectilinearGrid` object."""
1526        module = self.__class__.__module__
1527        name = self.__class__.__name__
1528        out = vedo.printc(
1529            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
1530            c="c", bold=True, invert=True, return_string=True,
1531        )
1532        out += "\x1b[0m\x1b[36;1m"
1533
1534        out += "name".ljust(14) + ": " + str(self.name) + "\n"
1535        if self.filename:
1536            out += "filename".ljust(14) + ": " + str(self.filename) + "\n"
1537
1538        out += "dimensions".ljust(14) + ": " + str(self.dataset.GetDimensions()) + "\n"
1539
1540        out += "center".ljust(14) + ": "
1541        out += utils.precision(self.dataset.GetCenter(), 6) + "\n"
1542
1543        bnds = self.bounds()
1544        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
1545        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
1546        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
1547        out += "bounds".ljust(14) + ":"
1548        out += " x=(" + bx1 + ", " + bx2 + "),"
1549        out += " y=(" + by1 + ", " + by2 + "),"
1550        out += " z=(" + bz1 + ", " + bz2 + ")\n"
1551
1552        out += "memory size".ljust(14) + ": "
1553        out += utils.precision(self.dataset.GetActualMemorySize() / 1024, 2) + " MB\n"
1554
1555        for key in self.pointdata.keys():
1556            arr = self.pointdata[key]
1557            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
1558            mark_active = "pointdata"
1559            a_scalars = self.dataset.GetPointData().GetScalars()
1560            a_vectors = self.dataset.GetPointData().GetVectors()
1561            a_tensors = self.dataset.GetPointData().GetTensors()
1562            if a_scalars and a_scalars.GetName() == key:
1563                mark_active += " *"
1564            elif a_vectors and a_vectors.GetName() == key:
1565                mark_active += " **"
1566            elif a_tensors and a_tensors.GetName() == key:
1567                mark_active += " ***"
1568            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
1569            out += f", range=({rng})\n"
1570
1571        for key in self.celldata.keys():
1572            arr = self.celldata[key]
1573            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
1574            mark_active = "celldata"
1575            a_scalars = self.dataset.GetCellData().GetScalars()
1576            a_vectors = self.dataset.GetCellData().GetVectors()
1577            a_tensors = self.dataset.GetCellData().GetTensors()
1578            if a_scalars and a_scalars.GetName() == key:
1579                mark_active += " *"
1580            elif a_vectors and a_vectors.GetName() == key:
1581                mark_active += " **"
1582            elif a_tensors and a_tensors.GetName() == key:
1583                mark_active += " ***"
1584            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
1585            out += f", range=({rng})\n"
1586
1587        for key in self.metadata.keys():
1588            arr = self.metadata[key]
1589            out += "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
1590
1591        return out.rstrip() + "\x1b[0m"
1592
1593    def _repr_html_(self):
1594        """
1595        HTML representation of the RectilinearGrid object for Jupyter Notebooks.
1596
1597        Returns:
1598            HTML text with the image and some properties.
1599        """
1600        import io
1601        import base64
1602        from PIL import Image
1603
1604        library_name = "vedo.grids.RectilinearGrid"
1605        help_url = "https://vedo.embl.es/docs/vedo/grids.html#RectilinearGrid"
1606
1607        m = self.tomesh().linewidth(1).lighting("off")
1608        arr= m.thumbnail(zoom=1, elevation=-30, azimuth=-30)
1609
1610        im = Image.fromarray(arr)
1611        buffered = io.BytesIO()
1612        im.save(buffered, format="PNG", quality=100)
1613        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
1614        url = "data:image/png;base64," + encoded
1615        image = f"<img src='{url}'></img>"
1616
1617        bounds = "<br/>".join(
1618            [
1619                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
1620                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
1621            ]
1622        )
1623
1624        help_text = ""
1625        if self.name:
1626            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
1627        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
1628        if self.filename:
1629            dots = ""
1630            if len(self.filename) > 30:
1631                dots = "..."
1632            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
1633
1634        pdata = ""
1635        if self.dataset.GetPointData().GetScalars():
1636            if self.dataset.GetPointData().GetScalars().GetName():
1637                name = self.dataset.GetPointData().GetScalars().GetName()
1638                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
1639
1640        cdata = ""
1641        if self.dataset.GetCellData().GetScalars():
1642            if self.dataset.GetCellData().GetScalars().GetName():
1643                name = self.dataset.GetCellData().GetScalars().GetName()
1644                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
1645
1646        pts = self.vertices
1647        cm = np.mean(pts, axis=0)
1648
1649        all = [
1650            "<table>",
1651            "<tr>",
1652            "<td>", image, "</td>",
1653            "<td style='text-align: center; vertical-align: center;'><br/>", help_text,
1654            "<table>",
1655            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
1656            "<tr><td><b> center of mass </b></td><td>" + utils.precision(cm,3) + "</td></tr>",
1657            "<tr><td><b> nr. points&nbsp/&nbspcells </b></td><td>"
1658            + str(self.npoints) + "&nbsp/&nbsp" + str(self.ncells) + "</td></tr>",
1659            pdata,
1660            cdata,
1661            "</table>",
1662            "</table>",
1663        ]
1664        return "\n".join(all)
1665
1666    def dimensions(self) -> np.ndarray:
1667        """Return the number of points in the x, y and z directions."""
1668        return np.array(self.dataset.GetDimensions())
1669
1670    def x_coordinates(self) -> np.ndarray:
1671        """Return the x-coordinates of the grid."""
1672        return utils.vtk2numpy(self.dataset.GetXCoordinates())
1673    
1674    def y_coordinates(self) -> np.ndarray:
1675        """Return the y-coordinates of the grid."""
1676        return utils.vtk2numpy(self.dataset.GetYCoordinates())
1677    
1678    def z_coordinates(self) -> np.ndarray:
1679        """Return the z-coordinates of the grid."""
1680        return utils.vtk2numpy(self.dataset.GetZCoordinates())
1681    
1682    def is_point_visible(self, pid: int) -> bool:
1683        """Return True if point `pid` is visible."""
1684        return self.dataset.IsPointVisible(pid)
1685    
1686    def is_cell_visible(self, cid: int) -> bool:
1687        """Return True if cell `cid` is visible."""
1688        return self.dataset.IsCellVisible(cid)
1689    
1690    def has_blank_points(self) -> bool:
1691        """Return True if the grid has blank points."""
1692        return self.dataset.HasAnyBlankPoints()
1693    
1694    def has_blank_cells(self) -> bool:
1695        """Return True if the grid has blank cells."""
1696        return self.dataset.HasAnyBlankCells()
1697    
1698    def compute_structured_coords(self, x: list) -> dict:
1699        """
1700        Convenience function computes the structured coordinates for a point `x`.
1701
1702        This method returns a dictionary with keys `ijk`, `pcoords` and `inside`.
1703        The cell is specified by the array `ijk`.
1704        and the parametric coordinates in the cell are specified with `pcoords`. 
1705        Value of `inside` is False if the point x is outside of the grid.
1706        """
1707        ijk = [0, 0, 0]
1708        pcoords = [0., 0., 0.]
1709        inout = self.dataset.ComputeStructuredCoordinates(x, ijk, pcoords)
1710        return {"ijk": np.array(ijk), "pcoords": np.array(pcoords), "inside": bool(inout)}
1711    
1712    def compute_pointid(self, ijk: int) -> int:
1713        """Given a location in structured coordinates (i-j-k), return the point id."""
1714        return self.dataset.ComputePointId(ijk)
1715    
1716    def compute_cellid(self, ijk: int) -> int:
1717        """Given a location in structured coordinates (i-j-k), return the cell id."""
1718        return self.dataset.ComputeCellId(ijk)
1719    
1720    def find_point(self, x: list) -> int:
1721        """Given a position `x`, return the id of the closest point."""
1722        return self.dataset.FindPoint(x)
1723    
1724    def find_cell(self, x: list) -> dict:
1725        """Given a position `x`, return the id of the closest cell."""
1726        cell = vtki.vtkHexagonalPrism()
1727        cellid = vtki.mutable(0)
1728        tol2 = 0.001 # vtki.mutable(0)
1729        subid = vtki.mutable(0)
1730        pcoords = [0.0, 0.0, 0.0]
1731        weights = [0.0, 0.0, 0.0]
1732        res = self.dataset.FindCell(x, cell, cellid, tol2, subid, pcoords, weights)
1733        result = {}
1734        result["cellid"] = cellid
1735        result["subid"] = subid
1736        result["pcoords"] = pcoords
1737        result["weights"] = weights
1738        result["status"] = res
1739        return result
1740
1741    def clone(self, deep=True) -> "RectilinearGrid":
1742        """Return a clone copy of the RectilinearGrid. Alias of `copy()`."""
1743        if deep:
1744            newrg = vtki.vtkRectilinearGrid()
1745            newrg.CopyStructure(self.dataset)
1746            newrg.CopyAttributes(self.dataset)
1747            newvol = RectilinearGrid(newrg)
1748        else:
1749            newvol = RectilinearGrid(self.dataset)
1750
1751        prop = vtki.vtkProperty()
1752        prop.DeepCopy(self.properties)
1753        newvol.actor.SetProperty(prop)
1754        newvol.properties = prop
1755        newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond")
1756        return newvol
1757
1758    def bounds(self) -> np.ndarray:
1759        """
1760        Get the object bounds.
1761        Returns a list in format `[xmin,xmax, ymin,ymax, zmin,zmax]`.
1762        """
1763        # OVERRIDE CommonAlgorithms.bounds() which is too slow
1764        return np.array(self.dataset.GetBounds())
1765
1766    def isosurface(self, value=None) -> "vedo.Mesh":
1767        """
1768        Return a `Mesh` isosurface extracted from the object.
1769
1770        Set `value` as single float or list of values to draw the isosurface(s).
1771        """
1772        scrange = self.dataset.GetScalarRange()
1773
1774        cf = vtki.new("ContourFilter")
1775        cf.UseScalarTreeOn()
1776        cf.SetInputData(self.dataset)
1777        cf.ComputeNormalsOn()
1778
1779        if value is None:
1780            value = (2 * scrange[0] + scrange[1]) / 3.0
1781            # print("automatic isosurface value =", value)
1782            cf.SetValue(0, value)
1783        else:
1784            if utils.is_sequence(value):
1785                cf.SetNumberOfContours(len(value))
1786                for i, t in enumerate(value):
1787                    cf.SetValue(i, t)
1788            else:
1789                cf.SetValue(0, value)
1790
1791        cf.Update()
1792        poly = cf.GetOutput()
1793
1794        out = vedo.mesh.Mesh(poly, c=None).phong()
1795        out.mapper.SetScalarRange(scrange[0], scrange[1])
1796
1797        out.pipeline = utils.OperationNode(
1798            "isosurface",
1799            parents=[self],
1800            comment=f"#pts {out.dataset.GetNumberOfPoints()}",
1801            c="#4cc9f0:#e9c46a",
1802        )
1803        return out
1804
1805    def cut_with_plane(self, origin=(0, 0, 0), normal="x") -> "UnstructuredGrid":
1806        """
1807        Cut the object with the plane defined by a point and a normal.
1808
1809        Arguments:
1810            origin : (list)
1811                the cutting plane goes through this point
1812            normal : (list, str)
1813                normal vector to the cutting plane
1814        """
1815        strn = str(normal)
1816        if strn   ==  "x": normal = (1, 0, 0)
1817        elif strn ==  "y": normal = (0, 1, 0)
1818        elif strn ==  "z": normal = (0, 0, 1)
1819        elif strn == "-x": normal = (-1, 0, 0)
1820        elif strn == "-y": normal = (0, -1, 0)
1821        elif strn == "-z": normal = (0, 0, -1)
1822        plane = vtki.new("Plane")
1823        plane.SetOrigin(origin)
1824        plane.SetNormal(normal)
1825        clipper = vtki.new("ClipDataSet")
1826        clipper.SetInputData(self.dataset)
1827        clipper.SetClipFunction(plane)
1828        clipper.GenerateClipScalarsOff()
1829        clipper.GenerateClippedOutputOff()
1830        clipper.SetValue(0)
1831        clipper.Update()
1832        cout = clipper.GetOutput()
1833        ug = vedo.UnstructuredGrid(cout)
1834        if isinstance(self, UnstructuredGrid):
1835            self._update(cout)
1836            self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
1837            return self
1838        ug.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
1839        return ug
1840
1841    def cut_with_mesh(self, mesh, invert=False, whole_cells=False, on_boundary=False) -> "UnstructuredGrid":
1842        """
1843        Cut a `RectilinearGrid` with a `Mesh`.
1844
1845        Use `invert` to return cut off part of the input object.
1846        """
1847        ug = self.dataset
1848
1849        ippd = vtki.new("ImplicitPolyDataDistance")
1850        ippd.SetInput(mesh.dataset)
1851
1852        if whole_cells or on_boundary:
1853            clipper = vtki.new("ExtractGeometry")
1854            clipper.SetInputData(ug)
1855            clipper.SetImplicitFunction(ippd)
1856            clipper.SetExtractInside(not invert)
1857            clipper.SetExtractBoundaryCells(False)
1858            if on_boundary:
1859                clipper.SetExtractBoundaryCells(True)
1860                clipper.SetExtractOnlyBoundaryCells(True)
1861        else:
1862            signed_dists = vtki.vtkFloatArray()
1863            signed_dists.SetNumberOfComponents(1)
1864            signed_dists.SetName("SignedDistance")
1865            for pointId in range(ug.GetNumberOfPoints()):
1866                p = ug.GetPoint(pointId)
1867                signed_dist = ippd.EvaluateFunction(p)
1868                signed_dists.InsertNextValue(signed_dist)
1869            ug.GetPointData().AddArray(signed_dists)
1870            ug.GetPointData().SetActiveScalars("SignedDistance")  # NEEDED
1871            clipper = vtki.new("ClipDataSet")
1872            clipper.SetInputData(ug)
1873            clipper.SetInsideOut(not invert)
1874            clipper.SetValue(0.0)
1875
1876        clipper.Update()
1877
1878        out = UnstructuredGrid(clipper.GetOutput())
1879        out.pipeline = utils.OperationNode("cut_with_mesh", parents=[self], c="#9e2a2b")
1880        return out
1881
1882
1883##########################################################################
1884class StructuredGrid(PointAlgorithms, MeshVisual):
1885    """
1886    Build a structured grid.
1887    """
1888
1889    def __init__(self, inputobj=None):
1890        """
1891        A StructuredGrid is a dataset where edges of the hexahedrons are 
1892        not necessarily parallel to the coordinate axes.
1893        It can be thought of as a tessellation of a block of 3D space,
1894        similar to a `RectilinearGrid`
1895        except that the cells are not necessarily cubes, they can have different
1896        orientations but are connected in the same way as a `RectilinearGrid`.
1897
1898        Arguments:
1899            inputobj : (vtkStructuredGrid, list, str)
1900                list of points and tet indices, or filename
1901        
1902        Example:
1903            ```python
1904            from vedo import *
1905            sgrid = StructuredGrid(dataurl+"structgrid.vts")
1906            print(sgrid)
1907            msh = sgrid.tomesh().lw(1)
1908            show(msh, axes=1, viewup="z")
1909            ```
1910
1911            ```python
1912            from vedo import *
1913
1914            cx = np.sqrt(np.linspace(100, 400, 10))
1915            cy = np.linspace(30, 40, 20)
1916            cz = np.linspace(40, 50, 30)
1917            x, y, z = np.meshgrid(cx, cy, cz)
1918
1919            sgrid1 = StructuredGrid([x, y, z])
1920            sgrid1.cmap("viridis", sgrid1.vertices[:, 0])
1921            print(sgrid1)
1922
1923            sgrid2 = sgrid1.clone().cut_with_plane(normal=(-1,1,1), origin=[14,34,44])
1924            msh2 = sgrid2.tomesh(shrink=0.9).lw(1).cmap("viridis")
1925
1926            show(
1927                [["StructuredGrid", sgrid1], ["Shrinked Mesh", msh2]],
1928                N=2, axes=1, viewup="z",
1929            )
1930            ```
1931        """
1932
1933        super().__init__()
1934
1935        self.dataset = None
1936
1937        self.mapper = vtki.new("PolyDataMapper")
1938        self._actor = vtki.vtkActor()
1939        self._actor.retrieve_object = weak_ref_to(self)
1940        self._actor.SetMapper(self.mapper)
1941        self.properties = self._actor.GetProperty()
1942
1943        self.transform = LinearTransform()
1944        self.point_locator = None
1945        self.cell_locator = None
1946        self.line_locator = None
1947
1948        self.name = "StructuredGrid"
1949        self.filename = ""
1950
1951        self.info = {}
1952        self.time =  time.time()
1953
1954        ###############################
1955        if inputobj is None:
1956            self.dataset = vtki.vtkStructuredGrid()
1957
1958        elif isinstance(inputobj, vtki.vtkStructuredGrid):
1959            self.dataset = inputobj
1960
1961        elif isinstance(inputobj, StructuredGrid):
1962            self.dataset = inputobj.dataset
1963
1964        elif isinstance(inputobj, str):
1965            if "https://" in inputobj:
1966                inputobj = download(inputobj, verbose=False)
1967            if inputobj.endswith(".vts"):
1968                reader = vtki.new("XMLStructuredGridReader")
1969            else:
1970                reader = vtki.new("StructuredGridReader")
1971            self.filename = inputobj
1972            reader.SetFileName(inputobj)
1973            reader.Update()
1974            self.dataset = reader.GetOutput()
1975        
1976        elif utils.is_sequence(inputobj):
1977            self.dataset = vtki.vtkStructuredGrid()
1978            x, y, z = inputobj
1979            xyz = np.vstack((
1980                x.flatten(order="F"),
1981                y.flatten(order="F"),
1982                z.flatten(order="F"))
1983            ).T
1984            dims = x.shape
1985            self.dataset.SetDimensions(dims)      
1986            # self.dataset.SetDimensions(dims[1], dims[0], dims[2])
1987            vpoints = vtki.vtkPoints()
1988            vpoints.SetData(utils.numpy2vtk(xyz))
1989            self.dataset.SetPoints(vpoints)
1990
1991
1992        ###############################
1993        if not self.dataset:
1994            vedo.logger.error(f"StructuredGrid: cannot understand input type {type(inputobj)}")
1995            return
1996
1997        self.properties.SetColor(0.352, 0.612, 0.996)  # blue7
1998
1999        self.pipeline = utils.OperationNode(
2000            self, comment=f"#tets {self.dataset.GetNumberOfCells()}", c="#9e2a2b"
2001        )
2002
2003    @property
2004    def actor(self):
2005        """Return the `vtkActor` of the object."""
2006        gf = vtki.new("GeometryFilter")
2007        gf.SetInputData(self.dataset)
2008        gf.Update()
2009        self.mapper.SetInputData(gf.GetOutput())
2010        self.mapper.Modified()
2011        return self._actor
2012
2013    @actor.setter
2014    def actor(self, _):
2015        pass
2016
2017    def _update(self, data, reset_locators=False):
2018        self.dataset = data
2019        if reset_locators:
2020            self.cell_locator = None
2021            self.point_locator = None
2022        return self
2023
2024    ##################################################################
2025    def __str__(self):
2026        """Print a summary for the `StructuredGrid` object."""
2027        module = self.__class__.__module__
2028        name = self.__class__.__name__
2029        out = vedo.printc(
2030            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
2031            c="c", bold=True, invert=True, return_string=True,
2032        )
2033        out += "\x1b[0m\x1b[36;1m"
2034
2035        out += "name".ljust(14) + ": " + str(self.name) + "\n"
2036        if self.filename:
2037            out += "filename".ljust(14) + ": " + str(self.filename) + "\n"
2038
2039        out += "dimensions".ljust(14) + ": " + str(self.dataset.GetDimensions()) + "\n"
2040
2041        out += "center".ljust(14) + ": "
2042        out += utils.precision(self.dataset.GetCenter(), 6) + "\n"
2043
2044        bnds = self.bounds()
2045        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
2046        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
2047        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
2048        out += "bounds".ljust(14) + ":"
2049        out += " x=(" + bx1 + ", " + bx2 + "),"
2050        out += " y=(" + by1 + ", " + by2 + "),"
2051        out += " z=(" + bz1 + ", " + bz2 + ")\n"
2052
2053        out += "memory size".ljust(14) + ": "
2054        out += utils.precision(self.dataset.GetActualMemorySize() / 1024, 2) + " MB\n"
2055
2056        for key in self.pointdata.keys():
2057            arr = self.pointdata[key]
2058            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
2059            mark_active = "pointdata"
2060            a_scalars = self.dataset.GetPointData().GetScalars()
2061            a_vectors = self.dataset.GetPointData().GetVectors()
2062            a_tensors = self.dataset.GetPointData().GetTensors()
2063            if a_scalars and a_scalars.GetName() == key:
2064                mark_active += " *"
2065            elif a_vectors and a_vectors.GetName() == key:
2066                mark_active += " **"
2067            elif a_tensors and a_tensors.GetName() == key:
2068                mark_active += " ***"
2069            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
2070            out += f", range=({rng})\n"
2071
2072        for key in self.celldata.keys():
2073            arr = self.celldata[key]
2074            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
2075            mark_active = "celldata"
2076            a_scalars = self.dataset.GetCellData().GetScalars()
2077            a_vectors = self.dataset.GetCellData().GetVectors()
2078            a_tensors = self.dataset.GetCellData().GetTensors()
2079            if a_scalars and a_scalars.GetName() == key:
2080                mark_active += " *"
2081            elif a_vectors and a_vectors.GetName() == key:
2082                mark_active += " **"
2083            elif a_tensors and a_tensors.GetName() == key:
2084                mark_active += " ***"
2085            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
2086            out += f", range=({rng})\n"
2087
2088        for key in self.metadata.keys():
2089            arr = self.metadata[key]
2090            out += "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
2091
2092        return out.rstrip() + "\x1b[0m"
2093    
2094    def _repr_html_(self):
2095        """
2096        HTML representation of the StructuredGrid object for Jupyter Notebooks.
2097
2098        Returns:
2099            HTML text with the image and some properties.
2100        """
2101        import io
2102        import base64
2103        from PIL import Image
2104
2105        library_name = "vedo.grids.StructuredGrid"
2106        help_url = "https://vedo.embl.es/docs/vedo/grids.html#StructuredGrid"
2107
2108        m = self.tomesh().linewidth(1).lighting("off")
2109        arr= m.thumbnail(zoom=1, elevation=-30, azimuth=-30)
2110
2111        im = Image.fromarray(arr)
2112        buffered = io.BytesIO()
2113        im.save(buffered, format="PNG", quality=100)
2114        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
2115        url = "data:image/png;base64," + encoded
2116        image = f"<img src='{url}'></img>"
2117
2118        bounds = "<br/>".join(
2119            [
2120                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
2121                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
2122            ]
2123        )
2124
2125        help_text = ""
2126        if self.name:
2127            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
2128        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
2129        if self.filename:
2130            dots = ""
2131            if len(self.filename) > 30:
2132                dots = "..."
2133            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
2134
2135        pdata = ""
2136        if self.dataset.GetPointData().GetScalars():
2137            if self.dataset.GetPointData().GetScalars().GetName():
2138                name = self.dataset.GetPointData().GetScalars().GetName()
2139                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
2140
2141        cdata = ""
2142        if self.dataset.GetCellData().GetScalars():
2143            if self.dataset.GetCellData().GetScalars().GetName():
2144                name = self.dataset.GetCellData().GetScalars().GetName()
2145                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
2146
2147        pts = self.vertices
2148        cm = np.mean(pts, axis=0)
2149
2150        all = [
2151            "<table>",
2152            "<tr>",
2153            "<td>", image, "</td>",
2154            "<td style='text-align: center; vertical-align: center;'><br/>", help_text,
2155            "<table>",
2156            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
2157            "<tr><td><b> center of mass </b></td><td>" + utils.precision(cm,3) + "</td></tr>",
2158            "<tr><td><b> nr. points&nbsp/&nbspcells </b></td><td>"
2159            + str(self.npoints) + "&nbsp/&nbsp" + str(self.ncells) + "</td></tr>",
2160            pdata,
2161            cdata,
2162            "</table>",
2163            "</table>",
2164        ]
2165        return "\n".join(all)
2166
2167    def dimensions(self) -> np.ndarray:
2168        """Return the number of points in the x, y and z directions."""
2169        return np.array(self.dataset.GetDimensions())
2170
2171    def clone(self, deep=True) -> "StructuredGrid":
2172        """Return a clone copy of the StructuredGrid. Alias of `copy()`."""
2173        if deep:
2174            newrg = vtki.vtkStructuredGrid()
2175            newrg.CopyStructure(self.dataset)
2176            newrg.CopyAttributes(self.dataset)
2177            newvol = StructuredGrid(newrg)
2178        else:
2179            newvol = StructuredGrid(self.dataset)
2180
2181        prop = vtki.vtkProperty()
2182        prop.DeepCopy(self.properties)
2183        newvol.actor.SetProperty(prop)
2184        newvol.properties = prop
2185        newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond")
2186        return newvol
2187
2188    def find_point(self, x: list) -> int:
2189        """Given a position `x`, return the id of the closest point."""
2190        return self.dataset.FindPoint(x)
2191    
2192    def find_cell(self, x: list) -> dict:
2193        """Given a position `x`, return the id of the closest cell."""
2194        cell = vtki.vtkHexagonalPrism()
2195        cellid = vtki.mutable(0)
2196        tol2 = 0.001 # vtki.mutable(0)
2197        subid = vtki.mutable(0)
2198        pcoords = [0.0, 0.0, 0.0]
2199        weights = [0.0, 0.0, 0.0]
2200        res = self.dataset.FindCell(x, cell, cellid, tol2, subid, pcoords, weights)
2201        result = {}
2202        result["cellid"] = cellid
2203        result["subid"] = subid
2204        result["pcoords"] = pcoords
2205        result["weights"] = weights
2206        result["status"] = res
2207        return result
2208
2209    def cut_with_plane(self, origin=(0, 0, 0), normal="x") -> "vedo.UnstructuredGrid":
2210        """
2211        Cut the object with the plane defined by a point and a normal.
2212
2213        Arguments:
2214            origin : (list)
2215                the cutting plane goes through this point
2216            normal : (list, str)
2217                normal vector to the cutting plane
2218        
2219        Returns an `UnstructuredGrid` object.
2220        """
2221        strn = str(normal)
2222        if strn   ==  "x": normal = (1, 0, 0)
2223        elif strn ==  "y": normal = (0, 1, 0)
2224        elif strn ==  "z": normal = (0, 0, 1)
2225        elif strn == "-x": normal = (-1, 0, 0)
2226        elif strn == "-y": normal = (0, -1, 0)
2227        elif strn == "-z": normal = (0, 0, -1)
2228        plane = vtki.new("Plane")
2229        plane.SetOrigin(origin)
2230        plane.SetNormal(normal)
2231        clipper = vtki.new("ClipDataSet")
2232        clipper.SetInputData(self.dataset)
2233        clipper.SetClipFunction(plane)
2234        clipper.GenerateClipScalarsOff()
2235        clipper.GenerateClippedOutputOff()
2236        clipper.SetValue(0)
2237        clipper.Update()
2238        cout = clipper.GetOutput()
2239        ug = vedo.UnstructuredGrid(cout)
2240        if isinstance(self, vedo.UnstructuredGrid):
2241            self._update(cout)
2242            self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
2243            return self
2244        ug.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
2245        return ug
2246
2247    def cut_with_mesh(self, mesh: Mesh, invert=False, whole_cells=False, on_boundary=False) -> "UnstructuredGrid":
2248        """
2249        Cut a `RectilinearGrid` with a `Mesh`.
2250
2251        Use `invert` to return cut off part of the input object.
2252
2253        Returns an `UnstructuredGrid` object.
2254        """
2255        ug = self.dataset
2256
2257        ippd = vtki.new("ImplicitPolyDataDistance")
2258        ippd.SetInput(mesh.dataset)
2259
2260        if whole_cells or on_boundary:
2261            clipper = vtki.new("ExtractGeometry")
2262            clipper.SetInputData(ug)
2263            clipper.SetImplicitFunction(ippd)
2264            clipper.SetExtractInside(not invert)
2265            clipper.SetExtractBoundaryCells(False)
2266            if on_boundary:
2267                clipper.SetExtractBoundaryCells(True)
2268                clipper.SetExtractOnlyBoundaryCells(True)
2269        else:
2270            signed_dists = vtki.vtkFloatArray()
2271            signed_dists.SetNumberOfComponents(1)
2272            signed_dists.SetName("SignedDistance")
2273            for pointId in range(ug.GetNumberOfPoints()):
2274                p = ug.GetPoint(pointId)
2275                signed_dist = ippd.EvaluateFunction(p)
2276                signed_dists.InsertNextValue(signed_dist)
2277            ug.GetPointData().AddArray(signed_dists)
2278            ug.GetPointData().SetActiveScalars("SignedDistance")  # NEEDED
2279            clipper = vtki.new("ClipDataSet")
2280            clipper.SetInputData(ug)
2281            clipper.SetInsideOut(not invert)
2282            clipper.SetValue(0.0)
2283
2284        clipper.Update()
2285
2286        out = UnstructuredGrid(clipper.GetOutput())
2287        out.pipeline = utils.OperationNode("cut_with_mesh", parents=[self], c="#9e2a2b")
2288        return out
2289
2290    def isosurface(self, value=None) -> "vedo.Mesh":
2291        """
2292        Return a `Mesh` isosurface extracted from the object.
2293
2294        Set `value` as single float or list of values to draw the isosurface(s).
2295        """
2296        scrange = self.dataset.GetScalarRange()
2297
2298        cf = vtki.new("ContourFilter")
2299        cf.UseScalarTreeOn()
2300        cf.SetInputData(self.dataset)
2301        cf.ComputeNormalsOn()
2302
2303        if value is None:
2304            value = (2 * scrange[0] + scrange[1]) / 3.0
2305            # print("automatic isosurface value =", value)
2306            cf.SetValue(0, value)
2307        else:
2308            if utils.is_sequence(value):
2309                cf.SetNumberOfContours(len(value))
2310                for i, t in enumerate(value):
2311                    cf.SetValue(i, t)
2312            else:
2313                cf.SetValue(0, value)
2314
2315        cf.Update()
2316        poly = cf.GetOutput()
2317
2318        out = vedo.mesh.Mesh(poly, c=None).phong()
2319        out.mapper.SetScalarRange(scrange[0], scrange[1])
2320
2321        out.pipeline = utils.OperationNode(
2322            "isosurface",
2323            parents=[self],
2324            comment=f"#pts {out.dataset.GetNumberOfPoints()}",
2325            c="#4cc9f0:#e9c46a",
2326        )
2327        return out
class UnstructuredGrid(vedo.core.PointAlgorithms, vedo.visual.MeshVisual):
 38class UnstructuredGrid(PointAlgorithms, MeshVisual):
 39    """Support for UnstructuredGrid objects."""
 40
 41    def __init__(self, inputobj=None):
 42        """
 43        Support for UnstructuredGrid objects.
 44
 45        Arguments:
 46            inputobj : (list, vtkUnstructuredGrid, str)
 47                A list in the form `[points, cells, celltypes]`,
 48                or a vtkUnstructuredGrid object, or a filename
 49
 50        Celltypes are identified by the following 
 51        [convention](https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html).
 52        """
 53        super().__init__()
 54
 55        self.dataset = None
 56
 57        self.mapper = vtki.new("PolyDataMapper")
 58        self._actor = vtki.vtkActor()
 59        self._actor.retrieve_object = weak_ref_to(self)
 60        self._actor.SetMapper(self.mapper)
 61        self.properties = self._actor.GetProperty()
 62
 63        self.transform = LinearTransform()
 64        self.point_locator = None
 65        self.cell_locator = None
 66        self.line_locator = None
 67
 68        self.name = "UnstructuredGrid"
 69        self.filename = ""
 70        self.file_size = ""
 71
 72        self.info = {}
 73        self.time = time.time()
 74        self.rendered_at = set()
 75
 76        ######################################
 77        inputtype = str(type(inputobj))
 78
 79        if inputobj is None:
 80            self.dataset = vtki.vtkUnstructuredGrid()
 81
 82        elif utils.is_sequence(inputobj):
 83
 84            pts, cells, celltypes = inputobj
 85            assert len(cells) == len(celltypes)
 86
 87            self.dataset = vtki.vtkUnstructuredGrid()
 88
 89            if not utils.is_sequence(cells[0]):
 90                tets = []
 91                nf = cells[0] + 1
 92                for i, cl in enumerate(cells):
 93                    if i in (nf, 0):
 94                        k = i + 1
 95                        nf = cl + k
 96                        cell = [cells[j + k] for j in range(cl)]
 97                        tets.append(cell)
 98                cells = tets
 99
100            # This would fill the points and use those to define orientation
101            vpts = utils.numpy2vtk(pts, dtype=np.float32)
102            points = vtki.vtkPoints()
103            points.SetData(vpts)
104            self.dataset.SetPoints(points)
105
106            # Fill cells
107            # https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html
108            for i, ct in enumerate(celltypes):
109                if   ct == vtki.cell_types["VERTEX"]:
110                    cell = vtki.vtkVertex()
111                elif ct == vtki.cell_types["POLY_VERTEX"]:
112                    cell = vtki.vtkPolyVertex()
113                elif ct == vtki.cell_types["TETRA"]:
114                    cell = vtki.vtkTetra()
115                elif ct == vtki.cell_types["WEDGE"]:
116                    cell = vtki.vtkWedge()
117                elif ct == vtki.cell_types["LINE"]:
118                    cell = vtki.vtkLine()
119                elif ct == vtki.cell_types["POLY_LINE"]:
120                    cell = vtki.vtkPolyLine()
121                elif ct == vtki.cell_types["TRIANGLE"]:
122                    cell = vtki.vtkTriangle()
123                elif ct == vtki.cell_types["TRIANGLE_STRIP"]:
124                    cell = vtki.vtkTriangleStrip()
125                elif ct == vtki.cell_types["POLYGON"]:
126                    cell = vtki.vtkPolygon()
127                elif ct == vtki.cell_types["PIXEL"]:
128                    cell = vtki.vtkPixel()
129                elif ct == vtki.cell_types["QUAD"]:
130                    cell = vtki.vtkQuad()
131                elif ct == vtki.cell_types["VOXEL"]:
132                    cell = vtki.vtkVoxel()
133                elif ct == vtki.cell_types["PYRAMID"]:
134                    cell = vtki.vtkPyramid()
135                elif ct == vtki.cell_types["HEXAHEDRON"]:
136                    cell = vtki.vtkHexahedron()
137                elif ct == vtki.cell_types["HEXAGONAL_PRISM"]:
138                    cell = vtki.vtkHexagonalPrism()
139                elif ct == vtki.cell_types["PENTAGONAL_PRISM"]:
140                    cell = vtki.vtkPentagonalPrism()
141                elif ct == vtki.cell_types["QUADRATIC_TETRA"]:
142                    from vtkmodules.vtkCommonDataModel import vtkQuadraticTetra
143                    cell = vtkQuadraticTetra()
144                elif ct == vtki.cell_types["QUADRATIC_HEXAHEDRON"]:
145                    from vtkmodules.vtkCommonDataModel import vtkQuadraticHexahedron
146                    cell = vtkQuadraticHexahedron()
147                elif ct == vtki.cell_types["QUADRATIC_WEDGE"]:
148                    from vtkmodules.vtkCommonDataModel import vtkQuadraticWedge
149                    cell = vtkQuadraticWedge()
150                elif ct == vtki.cell_types["QUADRATIC_PYRAMID"]:
151                    from vtkmodules.vtkCommonDataModel import vtkQuadraticPyramid
152                    cell = vtkQuadraticPyramid()
153                elif ct == vtki.cell_types["QUADRATIC_LINEAR_QUAD"]:
154                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearQuad
155                    cell = vtkQuadraticLinearQuad()
156                elif ct == vtki.cell_types["QUADRATIC_LINEAR_WEDGE"]:
157                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearWedge
158                    cell = vtkQuadraticLinearWedge()
159                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_WEDGE"]:
160                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticWedge
161                    cell = vtkBiQuadraticQuadraticWedge()
162                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_HEXAHEDRON"]:
163                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticHexahedron
164                    cell = vtkBiQuadraticQuadraticHexahedron()
165                elif ct == vtki.cell_types["BIQUADRATIC_TRIANGLE"]:
166                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticTriangle
167                    cell = vtkBiQuadraticTriangle()
168                elif ct == vtki.cell_types["CUBIC_LINE"]:
169                    from vtkmodules.vtkCommonDataModel import vtkCubicLine
170                    cell = vtkCubicLine()
171                elif ct == vtki.cell_types["CONVEX_POINT_SET"]:
172                    from vtkmodules.vtkCommonDataModel import vtkConvexPointSet
173                    cell = vtkConvexPointSet()
174                elif ct == vtki.cell_types["POLYHEDRON"]:
175                    from vtkmodules.vtkCommonDataModel import vtkPolyhedron
176                    cell = vtkPolyhedron()
177                elif ct == vtki.cell_types["HIGHER_ORDER_TRIANGLE"]:
178                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTriangle
179                    cell = vtkHigherOrderTriangle()
180                elif ct == vtki.cell_types["HIGHER_ORDER_QUAD"]:
181                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderQuadrilateral
182                    cell = vtkHigherOrderQuadrilateral()
183                elif ct == vtki.cell_types["HIGHER_ORDER_TETRAHEDRON"]:
184                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTetra
185                    cell = vtkHigherOrderTetra()
186                elif ct == vtki.cell_types["HIGHER_ORDER_WEDGE"]:
187                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderWedge
188                    cell = vtkHigherOrderWedge()
189                elif ct == vtki.cell_types["HIGHER_ORDER_HEXAHEDRON"]:
190                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderHexahedron
191                    cell = vtkHigherOrderHexahedron()
192                elif ct == vtki.cell_types["LAGRANGE_CURVE"]:
193                    from vtkmodules.vtkCommonDataModel import vtkLagrangeCurve
194                    cell = vtkLagrangeCurve()
195                elif ct == vtki.cell_types["LAGRANGE_TRIANGLE"]:
196                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTriangle
197                    cell = vtkLagrangeTriangle()
198                elif ct == vtki.cell_types["LAGRANGE_QUADRILATERAL"]:
199                    from vtkmodules.vtkCommonDataModel import vtkLagrangeQuadrilateral
200                    cell = vtkLagrangeQuadrilateral()
201                elif ct == vtki.cell_types["LAGRANGE_TETRAHEDRON"]:
202                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTetra
203                    cell = vtkLagrangeTetra()
204                elif ct == vtki.cell_types["LAGRANGE_HEXAHEDRON"]:
205                    from vtkmodules.vtkCommonDataModel import vtkLagrangeHexahedron
206                    cell = vtkLagrangeHexahedron()
207                elif ct == vtki.cell_types["LAGRANGE_WEDGE"]:
208                    from vtkmodules.vtkCommonDataModel import vtkLagrangeWedge
209                    cell = vtkLagrangeWedge()
210                elif ct == vtki.cell_types["BEZIER_CURVE"]:
211                    from vtkmodules.vtkCommonDataModel import vtkBezierCurve
212                    cell = vtkBezierCurve()
213                elif ct == vtki.cell_types["BEZIER_TRIANGLE"]:
214                    from vtkmodules.vtkCommonDataModel import vtkBezierTriangle
215                    cell = vtkBezierTriangle()
216                elif ct == vtki.cell_types["BEZIER_QUADRILATERAL"]:
217                    from vtkmodules.vtkCommonDataModel import vtkBezierQuadrilateral
218                    cell = vtkBezierQuadrilateral()
219                elif ct == vtki.cell_types["BEZIER_TETRAHEDRON"]:
220                    from vtkmodules.vtkCommonDataModel import vtkBezierTetra
221                    cell = vtkBezierTetra()
222                elif ct == vtki.cell_types["BEZIER_HEXAHEDRON"]:
223                    from vtkmodules.vtkCommonDataModel import vtkBezierHexahedron
224                    cell = vtkBezierHexahedron()
225                elif ct == vtki.cell_types["BEZIER_WEDGE"]:
226                    from vtkmodules.vtkCommonDataModel import vtkBezierWedge
227                    cell = vtkBezierWedge()
228                else:
229                    vedo.logger.error(
230                        f"UnstructuredGrid: cell type {ct} not supported. Skip.")
231                    continue
232
233                cpids = cell.GetPointIds()
234                cell_conn = cells[i]
235                for j, pid in enumerate(cell_conn):
236                    cpids.SetId(j, pid)
237                self.dataset.InsertNextCell(ct, cpids)
238
239        elif "UnstructuredGrid" in inputtype:
240            self.dataset = inputobj
241
242        elif isinstance(inputobj, str):
243            if "https://" in inputobj:
244                inputobj = download(inputobj, verbose=False)
245            self.filename = inputobj
246            if inputobj.endswith(".vtu"):
247                reader = vtki.new("XMLUnstructuredGridReader")
248            else:
249                reader = vtki.new("UnstructuredGridReader")
250            self.filename = inputobj
251            reader.SetFileName(inputobj)
252            reader.Update()
253            self.dataset = reader.GetOutput()
254
255        else:
256            # this converts other types of vtk objects to UnstructuredGrid
257            apf = vtki.new("AppendFilter")
258            try:
259                apf.AddInputData(inputobj)
260            except TypeError:
261                apf.AddInputData(inputobj.dataset)
262            apf.Update()
263            self.dataset = apf.GetOutput()
264
265        self.properties.SetColor(0.89, 0.455, 0.671)  # pink7
266
267        self.pipeline = utils.OperationNode(
268            self, comment=f"#cells {self.dataset.GetNumberOfCells()}", c="#4cc9f0"
269        )
270
271    # ------------------------------------------------------------------
272    def __str__(self):
273        """Print a string summary of the `UnstructuredGrid` object."""
274        module = self.__class__.__module__
275        name = self.__class__.__name__
276        out = vedo.printc(
277            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
278            c="m", bold=True, invert=True, return_string=True,
279        )
280        out += "\x1b[0m\u001b[35m"
281
282        out += "nr. of verts".ljust(14) + ": " + str(self.npoints) + "\n"
283        out += "nr. of cells".ljust(14) + ": " + str(self.ncells)  + "\n"
284        ct_arr = np.unique(self.cell_types_array)
285        cnames = [k for k, v in vtki.cell_types.items() if v in ct_arr]
286        out += "cell types".ljust(14) + ": " + str(cnames) + "\n"
287
288        if self.npoints:
289            out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6)
290            out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n"
291            out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n"
292
293        bnds = self.bounds()
294        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
295        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
296        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
297        out += "bounds".ljust(14) + ":"
298        out += " x=(" + bx1 + ", " + bx2 + "),"
299        out += " y=(" + by1 + ", " + by2 + "),"
300        out += " z=(" + bz1 + ", " + bz2 + ")\n"
301
302        for key in self.pointdata.keys():
303            arr = self.pointdata[key]
304            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
305            mark_active = "pointdata"
306            a_scalars = self.dataset.GetPointData().GetScalars()
307            a_vectors = self.dataset.GetPointData().GetVectors()
308            a_tensors = self.dataset.GetPointData().GetTensors()
309            if a_scalars and a_scalars.GetName() == key:
310                mark_active += " *"
311            elif a_vectors and a_vectors.GetName() == key:
312                mark_active += " **"
313            elif a_tensors and a_tensors.GetName() == key:
314                mark_active += " ***"
315            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
316            out += f", range=({rng})\n"
317
318        for key in self.celldata.keys():
319            arr = self.celldata[key]
320            rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
321            mark_active = "celldata"
322            a_scalars = self.dataset.GetCellData().GetScalars()
323            a_vectors = self.dataset.GetCellData().GetVectors()
324            a_tensors = self.dataset.GetCellData().GetTensors()
325            if a_scalars and a_scalars.GetName() == key:
326                mark_active += " *"
327            elif a_vectors and a_vectors.GetName() == key:
328                mark_active += " **"
329            elif a_tensors and a_tensors.GetName() == key:
330                mark_active += " ***"
331            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), ndim={arr.ndim}'
332            out += f", range=({rng})\n"
333
334        for key in self.metadata.keys():
335            arr = self.metadata[key]
336            out += "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
337
338        return out.rstrip() + "\x1b[0m"
339
340    def _repr_html_(self):
341        """
342        HTML representation of the UnstructuredGrid object for Jupyter Notebooks.
343
344        Returns:
345            HTML text with the image and some properties.
346        """
347        import io
348        import base64
349        from PIL import Image
350
351        library_name = "vedo.grids.UnstructuredGrid"
352        help_url = "https://vedo.embl.es/docs/vedo/grids.html#UnstructuredGrid"
353
354        arr = self.thumbnail()
355        im = Image.fromarray(arr)
356        buffered = io.BytesIO()
357        im.save(buffered, format="PNG", quality=100)
358        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
359        url = "data:image/png;base64," + encoded
360        image = f"<img src='{url}'></img>"
361
362        bounds = "<br/>".join(
363            [
364                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
365                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
366            ]
367        )
368
369        help_text = ""
370        if self.name:
371            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
372        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
373        if self.filename:
374            dots = ""
375            if len(self.filename) > 30:
376                dots = "..."
377            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
378
379        pdata = ""
380        if self.dataset.GetPointData().GetScalars():
381            if self.dataset.GetPointData().GetScalars().GetName():
382                name = self.dataset.GetPointData().GetScalars().GetName()
383                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
384
385        cdata = ""
386        if self.dataset.GetCellData().GetScalars():
387            if self.dataset.GetCellData().GetScalars().GetName():
388                name = self.dataset.GetCellData().GetScalars().GetName()
389                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
390
391        pts = self.vertices
392        cm = np.mean(pts, axis=0)
393
394        all = [
395            "<table>",
396            "<tr>",
397            "<td>", image, "</td>",
398            "<td style='text-align: center; vertical-align: center;'><br/>", help_text,
399            "<table>",
400            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
401            "<tr><td><b> center of mass </b></td><td>" + utils.precision(cm,3) + "</td></tr>",
402            # "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>",
403            "<tr><td><b> nr. points&nbsp/&nbspcells </b></td><td>"
404            + str(self.npoints) + "&nbsp/&nbsp" + str(self.ncells) + "</td></tr>",
405            pdata,
406            cdata,
407            "</table>",
408            "</table>",
409        ]
410        return "\n".join(all)
411
412    @property
413    def actor(self):
414        """Return the `vtkActor` of the object."""
415        # print("building actor")
416        gf = vtki.new("GeometryFilter")
417        gf.SetInputData(self.dataset)
418        gf.Update()
419        out = gf.GetOutput()
420        self.mapper.SetInputData(out)
421        self.mapper.Modified()
422        return self._actor
423
424    @actor.setter
425    def actor(self, _):
426        pass
427
428    def _update(self, data, reset_locators=False):
429        self.dataset = data
430        if reset_locators:
431            self.cell_locator = None
432            self.point_locator = None
433        return self
434    
435    def merge(self, *others) -> Self:
436        """
437        Merge multiple datasets into one single `UnstrcturedGrid`.
438        """
439        apf = vtki.new("AppendFilter")
440        for o in others:
441            if isinstance(o, UnstructuredGrid):
442                apf.AddInputData(o.dataset)
443            elif isinstance(o, vtki.vtkUnstructuredGrid):
444                apf.AddInputData(o)
445            else:
446                vedo.printc("Error: cannot merge type", type(o), c="r")
447        apf.Update()
448        self._update(apf.GetOutput())
449        self.pipeline = utils.OperationNode(
450            "merge", parents=[self, *others], c="#9e2a2b"
451        )
452        return self
453
454    def copy(self, deep=True) -> "UnstructuredGrid":
455        """Return a copy of the object. Alias of `clone()`."""
456        return self.clone(deep=deep)
457
458    def clone(self, deep=True) -> "UnstructuredGrid":
459        """Clone the UnstructuredGrid object to yield an exact copy."""
460        ug = vtki.vtkUnstructuredGrid()
461        if deep:
462            ug.DeepCopy(self.dataset)
463        else:
464            ug.ShallowCopy(self.dataset)
465        if isinstance(self, vedo.UnstructuredGrid):
466            cloned = vedo.UnstructuredGrid(ug)
467        else:
468            cloned = vedo.TetMesh(ug)
469
470        cloned.copy_properties_from(self)
471
472        cloned.pipeline = utils.OperationNode(
473            "clone", parents=[self], shape="diamond", c="#bbe1ed"
474        )
475        return cloned
476
477    def bounds(self) -> np.ndarray:
478        """
479        Get the object bounds.
480        Returns a list in format `[xmin,xmax, ymin,ymax, zmin,zmax]`.
481        """
482        # OVERRIDE CommonAlgorithms.bounds() which is too slow
483        return np.array(self.dataset.GetBounds())
484
485    def threshold(self, name=None, above=None, below=None, on="cells") -> Self:
486        """
487        Threshold the tetrahedral mesh by a cell scalar value.
488        Reduce to only tets which satisfy the threshold limits.
489
490        - if `above = below` will only select tets with that specific value.
491        - if `above > below` selection range is flipped.
492
493        Set keyword "on" to either "cells" or "points".
494        """
495        th = vtki.new("Threshold")
496        th.SetInputData(self.dataset)
497
498        if name is None:
499            if self.celldata.keys():
500                name = self.celldata.keys()[0]
501                th.SetInputArrayToProcess(0, 0, 0, 1, name)
502            elif self.pointdata.keys():
503                name = self.pointdata.keys()[0]
504                th.SetInputArrayToProcess(0, 0, 0, 0, name)
505            if name is None:
506                vedo.logger.warning("cannot find active array. Skip.")
507                return self
508        else:
509            if on.startswith("c"):
510                th.SetInputArrayToProcess(0, 0, 0, 1, name)
511            else:
512                th.SetInputArrayToProcess(0, 0, 0, 0, name)
513
514        if above is not None:
515            th.SetLowerThreshold(above)
516
517        if below is not None:
518            th.SetUpperThreshold(below)
519
520        th.Update()
521        return self._update(th.GetOutput())
522
523    def isosurface(self, value=None, flying_edges=False) -> "vedo.mesh.Mesh":
524        """
525        Return an `Mesh` isosurface extracted from the `Volume` object.
526
527        Set `value` as single float or list of values to draw the isosurface(s).
528        Use flying_edges for faster results (but sometimes can interfere with `smooth()`).
529
530        Examples:
531            - [isosurfaces1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/isosurfaces1.py)
532
533                ![](https://vedo.embl.es/images/volumetric/isosurfaces.png)
534        """
535        scrange = self.dataset.GetScalarRange()
536
537        if flying_edges:
538            cf = vtki.new("FlyingEdges3D")
539            cf.InterpolateAttributesOn()
540        else:
541            cf = vtki.new("ContourFilter")
542            cf.UseScalarTreeOn()
543
544        cf.SetInputData(self.dataset)
545        cf.ComputeNormalsOn()
546
547        if utils.is_sequence(value):
548            cf.SetNumberOfContours(len(value))
549            for i, t in enumerate(value):
550                cf.SetValue(i, t)
551        else:
552            if value is None:
553                value = (2 * scrange[0] + scrange[1]) / 3.0
554                # print("automatic isosurface value =", value)
555            cf.SetValue(0, value)
556
557        cf.Update()
558        poly = cf.GetOutput()
559
560        out = vedo.mesh.Mesh(poly, c=None).flat()
561        out.mapper.SetScalarRange(scrange[0], scrange[1])
562
563        out.pipeline = utils.OperationNode(
564            "isosurface",
565            parents=[self],
566            comment=f"#pts {out.dataset.GetNumberOfPoints()}",
567            c="#4cc9f0:#e9c46a",
568        )
569        return out
570
571    def shrink(self, fraction=0.8) -> Self:
572        """
573        Shrink the individual cells.
574
575        ![](https://vedo.embl.es/images/feats/shrink_hex.png)
576        """
577        sf = vtki.new("ShrinkFilter")
578        sf.SetInputData(self.dataset)
579        sf.SetShrinkFactor(fraction)
580        sf.Update()
581        out = sf.GetOutput()
582        self._update(out)
583        self.pipeline = utils.OperationNode(
584            "shrink", comment=f"by {fraction}", parents=[self], c="#9e2a2b"
585        )
586        return self
587
588    def tomesh(self, fill=False, shrink=1.0) -> "vedo.mesh.Mesh":
589        """
590        Build a polygonal `Mesh` from the current object.
591
592        If `fill=True`, the interior faces of all the cells are created.
593        (setting a `shrink` value slightly smaller than the default 1.0
594        can avoid flickering due to internal adjacent faces).
595
596        If `fill=False`, only the boundary faces will be generated.
597        """
598        gf = vtki.new("GeometryFilter")
599        if fill:
600            sf = vtki.new("ShrinkFilter")
601            sf.SetInputData(self.dataset)
602            sf.SetShrinkFactor(shrink)
603            sf.Update()
604            gf.SetInputData(sf.GetOutput())
605            gf.Update()
606            poly = gf.GetOutput()
607        else:
608            gf.SetInputData(self.dataset)
609            gf.Update()
610            poly = gf.GetOutput()
611
612        msh = vedo.mesh.Mesh(poly)
613        msh.copy_properties_from(self)
614        msh.pipeline = utils.OperationNode(
615            "tomesh", parents=[self], comment=f"fill={fill}", c="#9e2a2b:#e9c46a"
616        )
617        return msh
618
619    @property
620    def cell_types_array(self):
621        """Return the list of cell types in the dataset."""
622        uarr = self.dataset.GetCellTypesArray()
623        return utils.vtk2numpy(uarr)
624
625    def extract_cells_by_type(self, ctype) -> "UnstructuredGrid":
626        """Extract a specific cell type and return a new `UnstructuredGrid`."""
627        if isinstance(ctype, str):
628            try:
629                ctype = vtki.cell_types[ctype.upper()]
630            except KeyError:
631                vedo.logger.error(f"extract_cells_by_type: cell type {ctype} does not exist. Skip.")
632                return self
633        uarr = self.dataset.GetCellTypesArray()
634        ctarrtyp = np.where(utils.vtk2numpy(uarr) == ctype)[0]
635        uarrtyp = utils.numpy2vtk(ctarrtyp, deep=False, dtype="id")
636        selection_node = vtki.new("SelectionNode")
637        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
638        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
639        selection_node.SetSelectionList(uarrtyp)
640        selection = vtki.new("Selection")
641        selection.AddNode(selection_node)
642        es = vtki.new("ExtractSelection")
643        es.SetInputData(0, self.dataset)
644        es.SetInputData(1, selection)
645        es.Update()
646
647        ug = UnstructuredGrid(es.GetOutput())
648        ug.pipeline = utils.OperationNode(
649            "extract_cell_type", comment=f"type {ctype}", c="#edabab", parents=[self]
650        )
651        return ug
652
653    def extract_cells_by_id(self, idlist, use_point_ids=False) -> "UnstructuredGrid":
654        """Return a new `UnstructuredGrid` composed of the specified subset of indices."""
655        selection_node = vtki.new("SelectionNode")
656        if use_point_ids:
657            selection_node.SetFieldType(vtki.get_class("SelectionNode").POINT)
658            contcells = vtki.get_class("SelectionNode").CONTAINING_CELLS()
659            selection_node.GetProperties().Set(contcells, 1)
660        else:
661            selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
662        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
663        vidlist = utils.numpy2vtk(idlist, dtype="id")
664        selection_node.SetSelectionList(vidlist)
665        selection = vtki.new("Selection")
666        selection.AddNode(selection_node)
667        es = vtki.new("ExtractSelection")
668        es.SetInputData(0, self)
669        es.SetInputData(1, selection)
670        es.Update()
671
672        ug = UnstructuredGrid(es.GetOutput())
673        pr = vtki.vtkProperty()
674        pr.DeepCopy(self.properties)
675        ug.actor.SetProperty(pr)
676        ug.properties = pr
677
678        ug.mapper.SetLookupTable(utils.ctf2lut(self))
679        ug.pipeline = utils.OperationNode(
680            "extract_cells_by_id",
681            parents=[self],
682            comment=f"#cells {self.dataset.GetNumberOfCells()}",
683            c="#9e2a2b",
684        )
685        return ug
686
687    def find_cell(self, p: list) -> int:
688        """Locate the cell that contains a point and return the cell ID."""
689        if self.cell_locator is None:
690            self.cell_locator = vtki.new("CellLocator")
691            self.cell_locator.SetDataSet(self.dataset)
692            self.cell_locator.BuildLocator()
693        cid = self.cell_locator.FindCell(p)
694        return cid
695
696    def clean(self) -> Self:
697        """
698        Cleanup unused points and empty cells
699        """
700        cl = vtki.new("StaticCleanUnstructuredGrid")
701        cl.SetInputData(self.dataset)
702        cl.RemoveUnusedPointsOn()
703        cl.ProduceMergeMapOff()
704        cl.AveragePointDataOff()
705        cl.Update()
706
707        self._update(cl.GetOutput())
708        self.pipeline = utils.OperationNode(
709            "clean",
710            parents=[self],
711            comment=f"#cells {self.dataset.GetNumberOfCells()}",
712            c="#9e2a2b",
713        )
714        return self
715
716    def extract_cells_on_plane(self, origin: tuple, normal: tuple) -> Self:
717        """
718        Extract cells that are lying of the specified surface.
719        """
720        bf = vtki.new("3DLinearGridCrinkleExtractor")
721        bf.SetInputData(self.dataset)
722        bf.CopyPointDataOn()
723        bf.CopyCellDataOn()
724        bf.RemoveUnusedPointsOff()
725
726        plane = vtki.new("Plane")
727        plane.SetOrigin(origin)
728        plane.SetNormal(normal)
729        bf.SetImplicitFunction(plane)
730        bf.Update()
731
732        self._update(bf.GetOutput(), reset_locators=False)
733        self.pipeline = utils.OperationNode(
734            "extract_cells_on_plane",
735            parents=[self],
736            comment=f"#cells {self.dataset.GetNumberOfCells()}",
737            c="#9e2a2b",
738        )
739        return self
740
741    def extract_cells_on_sphere(self, center: tuple, radius: tuple) -> Self:
742        """
743        Extract cells that are lying of the specified surface.
744        """
745        bf = vtki.new("3DLinearGridCrinkleExtractor")
746        bf.SetInputData(self.dataset)
747        bf.CopyPointDataOn()
748        bf.CopyCellDataOn()
749        bf.RemoveUnusedPointsOff()
750
751        sph = vtki.new("Sphere")
752        sph.SetRadius(radius)
753        sph.SetCenter(center)
754        bf.SetImplicitFunction(sph)
755        bf.Update()
756
757        self._update(bf.GetOutput())
758        self.pipeline = utils.OperationNode(
759            "extract_cells_on_sphere",
760            parents=[self],
761            comment=f"#cells {self.dataset.GetNumberOfCells()}",
762            c="#9e2a2b",
763        )
764        return self
765
766    def extract_cells_on_cylinder(self, center: tuple, axis: tuple, radius: float) -> Self:
767        """
768        Extract cells that are lying of the specified surface.
769        """
770        bf = vtki.new("3DLinearGridCrinkleExtractor")
771        bf.SetInputData(self.dataset)
772        bf.CopyPointDataOn()
773        bf.CopyCellDataOn()
774        bf.RemoveUnusedPointsOff()
775
776        cyl = vtki.new("Cylinder")
777        cyl.SetRadius(radius)
778        cyl.SetCenter(center)
779        cyl.SetAxis(axis)
780        bf.SetImplicitFunction(cyl)
781        bf.Update()
782
783        self.pipeline = utils.OperationNode(
784            "extract_cells_on_cylinder",
785            parents=[self],
786            comment=f"#cells {self.dataset.GetNumberOfCells()}",
787            c="#9e2a2b",
788        )
789        self._update(bf.GetOutput())
790        return self
791
792    def cut_with_plane(self, origin=(0, 0, 0), normal="x") -> "UnstructuredGrid":
793        """
794        Cut the object with the plane defined by a point and a normal.
795
796        Arguments:
797            origin : (list)
798                the cutting plane goes through this point
799            normal : (list, str)
800                normal vector to the cutting plane
801        """
802        # if isinstance(self, vedo.Volume):
803        #     raise RuntimeError("cut_with_plane() is not applicable to Volume objects.")
804
805        strn = str(normal)
806        if strn   ==  "x": normal = (1, 0, 0)
807        elif strn ==  "y": normal = (0, 1, 0)
808        elif strn ==  "z": normal = (0, 0, 1)
809        elif strn == "-x": normal = (-1, 0, 0)
810        elif strn == "-y": normal = (0, -1, 0)
811        elif strn == "-z": normal = (0, 0, -1)
812        plane = vtki.new("Plane")
813        plane.SetOrigin(origin)
814        plane.SetNormal(normal)
815        clipper = vtki.new("ClipDataSet")
816        clipper.SetInputData(self.dataset)
817        clipper.SetClipFunction(plane)
818        clipper.GenerateClipScalarsOff()
819        clipper.GenerateClippedOutputOff()
820        clipper.SetValue(0)
821        clipper.Update()
822        cout = clipper.GetOutput()
823
824        if isinstance(cout, vtki.vtkUnstructuredGrid):
825            ug = vedo.UnstructuredGrid(cout)
826            if isinstance(self, vedo.UnstructuredGrid):
827                self._update(cout)
828                self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
829                return self
830            ug.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
831            return ug
832
833        else:
834            self._update(cout)
835            self.pipeline = utils.OperationNode("cut_with_plane", parents=[self], c="#9e2a2b")
836            return self
837
838    def cut_with_box(self, box: Any) -> "UnstructuredGrid":
839        """
840        Cut the grid with the specified bounding box.
841
842        Parameter box has format [xmin, xmax, ymin, ymax, zmin, zmax].
843        If an object is passed, its bounding box are used.
844
845        This method always returns a TetMesh object.
846
847        Example:
848        ```python
849        from vedo import *
850        tmesh = TetMesh(dataurl+'limb_ugrid.vtk')
851        tmesh.color('rainbow')
852        cu = Cube(side=500).x(500) # any Mesh works
853        tmesh.cut_with_box(cu).show(axes=1)
854        ```
855
856        ![](https://vedo.embl.es/images/feats/tet_cut_box.png)
857        """
858        bc = vtki.new("BoxClipDataSet")
859        bc.SetInputData(self.dataset)
860        try:
861            boxb = box.bounds()
862        except AttributeError:
863            boxb = box
864
865        bc.SetBoxClip(*boxb)
866        bc.Update()
867        cout = bc.GetOutput()
868
869        # output of vtkBoxClipDataSet is always tetrahedrons
870        tm = vedo.TetMesh(cout)
871        tm.pipeline = utils.OperationNode("cut_with_box", parents=[self], c="#9e2a2b")
872        return tm
873
874    def cut_with_mesh(self, mesh: Mesh, invert=False, whole_cells=False, on_boundary=False) -> "UnstructuredGrid":
875        """
876        Cut a `UnstructuredGrid` or `TetMesh` with a `Mesh`.
877
878        Use `invert` to return cut off part of the input object.
879        """
880        ug = self.dataset
881
882        ippd = vtki.new("ImplicitPolyDataDistance")
883        ippd.SetInput(mesh.dataset)
884
885        if whole_cells or on_boundary:
886            clipper = vtki.new("ExtractGeometry")
887            clipper.SetInputData(ug)
888            clipper.SetImplicitFunction(ippd)
889            clipper.SetExtractInside(not invert)
890            clipper.SetExtractBoundaryCells(False)
891            if on_boundary:
892                clipper.SetExtractBoundaryCells(True)
893                clipper.SetExtractOnlyBoundaryCells(True)
894        else:
895            signed_dists = vtki.vtkFloatArray()
896            signed_dists.SetNumberOfComponents(1)
897            signed_dists.SetName("SignedDistance")
898            for pointId in range(ug.GetNumberOfPoints()):
899                p = ug.GetPoint(pointId)
900                signed_dist = ippd.EvaluateFunction(p)
901                signed_dists.InsertNextValue(signed_dist)
902            ug.GetPointData().AddArray(signed_dists)
903            ug.GetPointData().SetActiveScalars("SignedDistance")  # NEEDED
904            clipper = vtki.new("ClipDataSet")
905            clipper.SetInputData(ug)
906            clipper.SetInsideOut(not invert)
907            clipper.SetValue(0.0)
908
909        clipper.Update()
910
911        out = vedo.UnstructuredGrid(clipper.GetOutput())
912        out.pipeline = utils.OperationNode("cut_with_mesh", parents=[self], c="#9e2a2b")
913        return out

Support for UnstructuredGrid objects.

UnstructuredGrid(inputobj=None)
 41    def __init__(self, inputobj=None):
 42        """
 43        Support for UnstructuredGrid objects.
 44
 45        Arguments:
 46            inputobj : (list, vtkUnstructuredGrid, str)
 47                A list in the form `[points, cells, celltypes]`,
 48                or a vtkUnstructuredGrid object, or a filename
 49
 50        Celltypes are identified by the following 
 51        [convention](https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html).
 52        """
 53        super().__init__()
 54
 55        self.dataset = None
 56
 57        self.mapper = vtki.new("PolyDataMapper")
 58        self._actor = vtki.vtkActor()
 59        self._actor.retrieve_object = weak_ref_to(self)
 60        self._actor.SetMapper(self.mapper)
 61        self.properties = self._actor.GetProperty()
 62
 63        self.transform = LinearTransform()
 64        self.point_locator = None
 65        self.cell_locator = None
 66        self.line_locator = None
 67
 68        self.name = "UnstructuredGrid"
 69        self.filename = ""
 70        self.file_size = ""
 71
 72        self.info = {}
 73        self.time = time.time()
 74        self.rendered_at = set()
 75
 76        ######################################
 77        inputtype = str(type(inputobj))
 78
 79        if inputobj is None:
 80            self.dataset = vtki.vtkUnstructuredGrid()
 81
 82        elif utils.is_sequence(inputobj):
 83
 84            pts, cells, celltypes = inputobj
 85            assert len(cells) == len(celltypes)
 86
 87            self.dataset = vtki.vtkUnstructuredGrid()
 88
 89            if not utils.is_sequence(cells[0]):
 90                tets = []
 91                nf = cells[0] + 1
 92                for i, cl in enumerate(cells):
 93                    if i in (nf, 0):
 94                        k = i + 1
 95                        nf = cl + k
 96                        cell = [cells[j + k] for j in range(cl)]
 97                        tets.append(cell)
 98                cells = tets
 99
100            # This would fill the points and use those to define orientation
101            vpts = utils.numpy2vtk(pts, dtype=np.float32)
102            points = vtki.vtkPoints()
103            points.SetData(vpts)
104            self.dataset.SetPoints(points)
105
106            # Fill cells
107            # https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html
108            for i, ct in enumerate(celltypes):
109                if   ct == vtki.cell_types["VERTEX"]:
110                    cell = vtki.vtkVertex()
111                elif ct == vtki.cell_types["POLY_VERTEX"]:
112                    cell = vtki.vtkPolyVertex()
113                elif ct == vtki.cell_types["TETRA"]:
114                    cell = vtki.vtkTetra()
115                elif ct == vtki.cell_types["WEDGE"]:
116                    cell = vtki.vtkWedge()
117                elif ct == vtki.cell_types["LINE"]:
118                    cell = vtki.vtkLine()
119                elif ct == vtki.cell_types["POLY_LINE"]:
120                    cell = vtki.vtkPolyLine()
121                elif ct == vtki.cell_types["TRIANGLE"]:
122                    cell = vtki.vtkTriangle()
123                elif ct == vtki.cell_types["TRIANGLE_STRIP"]:
124                    cell = vtki.vtkTriangleStrip()
125                elif ct == vtki.cell_types["POLYGON"]:
126                    cell = vtki.vtkPolygon()
127                elif ct == vtki.cell_types["PIXEL"]:
128                    cell = vtki.vtkPixel()
129                elif ct == vtki.cell_types["QUAD"]:
130                    cell = vtki.vtkQuad()
131                elif ct == vtki.cell_types["VOXEL"]:
132                    cell = vtki.vtkVoxel()
133                elif ct == vtki.cell_types["PYRAMID"]:
134                    cell = vtki.vtkPyramid()
135                elif ct == vtki.cell_types["HEXAHEDRON"]:
136                    cell = vtki.vtkHexahedron()
137                elif ct == vtki.cell_types["HEXAGONAL_PRISM"]:
138                    cell = vtki.vtkHexagonalPrism()
139                elif ct == vtki.cell_types["PENTAGONAL_PRISM"]:
140                    cell = vtki.vtkPentagonalPrism()
141                elif ct == vtki.cell_types["QUADRATIC_TETRA"]:
142                    from vtkmodules.vtkCommonDataModel import vtkQuadraticTetra
143                    cell = vtkQuadraticTetra()
144                elif ct == vtki.cell_types["QUADRATIC_HEXAHEDRON"]:
145                    from vtkmodules.vtkCommonDataModel import vtkQuadraticHexahedron
146                    cell = vtkQuadraticHexahedron()
147                elif ct == vtki.cell_types["QUADRATIC_WEDGE"]:
148                    from vtkmodules.vtkCommonDataModel import vtkQuadraticWedge
149                    cell = vtkQuadraticWedge()
150                elif ct == vtki.cell_types["QUADRATIC_PYRAMID"]:
151                    from vtkmodules.vtkCommonDataModel import vtkQuadraticPyramid
152                    cell = vtkQuadraticPyramid()
153                elif ct == vtki.cell_types["QUADRATIC_LINEAR_QUAD"]:
154                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearQuad
155                    cell = vtkQuadraticLinearQuad()
156                elif ct == vtki.cell_types["QUADRATIC_LINEAR_WEDGE"]:
157                    from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearWedge
158                    cell = vtkQuadraticLinearWedge()
159                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_WEDGE"]:
160                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticWedge
161                    cell = vtkBiQuadraticQuadraticWedge()
162                elif ct == vtki.cell_types["BIQUADRATIC_QUADRATIC_HEXAHEDRON"]:
163                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuadraticHexahedron
164                    cell = vtkBiQuadraticQuadraticHexahedron()
165                elif ct == vtki.cell_types["BIQUADRATIC_TRIANGLE"]:
166                    from vtkmodules.vtkCommonDataModel import vtkBiQuadraticTriangle
167                    cell = vtkBiQuadraticTriangle()
168                elif ct == vtki.cell_types["CUBIC_LINE"]:
169                    from vtkmodules.vtkCommonDataModel import vtkCubicLine
170                    cell = vtkCubicLine()
171                elif ct == vtki.cell_types["CONVEX_POINT_SET"]:
172                    from vtkmodules.vtkCommonDataModel import vtkConvexPointSet
173                    cell = vtkConvexPointSet()
174                elif ct == vtki.cell_types["POLYHEDRON"]:
175                    from vtkmodules.vtkCommonDataModel import vtkPolyhedron
176                    cell = vtkPolyhedron()
177                elif ct == vtki.cell_types["HIGHER_ORDER_TRIANGLE"]:
178                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTriangle
179                    cell = vtkHigherOrderTriangle()
180                elif ct == vtki.cell_types["HIGHER_ORDER_QUAD"]:
181                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderQuadrilateral
182                    cell = vtkHigherOrderQuadrilateral()
183                elif ct == vtki.cell_types["HIGHER_ORDER_TETRAHEDRON"]:
184                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderTetra
185                    cell = vtkHigherOrderTetra()
186                elif ct == vtki.cell_types["HIGHER_ORDER_WEDGE"]:
187                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderWedge
188                    cell = vtkHigherOrderWedge()
189                elif ct == vtki.cell_types["HIGHER_ORDER_HEXAHEDRON"]:
190                    from vtkmodules.vtkCommonDataModel import vtkHigherOrderHexahedron
191                    cell = vtkHigherOrderHexahedron()
192                elif ct == vtki.cell_types["LAGRANGE_CURVE"]:
193                    from vtkmodules.vtkCommonDataModel import vtkLagrangeCurve
194                    cell = vtkLagrangeCurve()
195                elif ct == vtki.cell_types["LAGRANGE_TRIANGLE"]:
196                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTriangle
197                    cell = vtkLagrangeTriangle()
198                elif ct == vtki.cell_types["LAGRANGE_QUADRILATERAL"]:
199                    from vtkmodules.vtkCommonDataModel import vtkLagrangeQuadrilateral
200                    cell = vtkLagrangeQuadrilateral()
201                elif ct == vtki.cell_types["LAGRANGE_TETRAHEDRON"]:
202                    from vtkmodules.vtkCommonDataModel import vtkLagrangeTetra
203                    cell = vtkLagrangeTetra()
204                elif ct == vtki.cell_types["LAGRANGE_HEXAHEDRON"]:
205                    from vtkmodules.vtkCommonDataModel import vtkLagrangeHexahedron
206                    cell = vtkLagrangeHexahedron()
207                elif ct == vtki.cell_types["LAGRANGE_WEDGE"]:
208                    from vtkmodules.vtkCommonDataModel import vtkLagrangeWedge
209                    cell = vtkLagrangeWedge()
210                elif ct == vtki.cell_types["BEZIER_CURVE"]:
211                    from vtkmodules.vtkCommonDataModel import vtkBezierCurve
212                    cell = vtkBezierCurve()
213                elif ct == vtki.cell_types["BEZIER_TRIANGLE"]:
214                    from vtkmodules.vtkCommonDataModel import vtkBezierTriangle
215                    cell = vtkBezierTriangle()
216                elif ct == vtki.cell_types["BEZIER_QUADRILATERAL"]:
217                    from vtkmodules.vtkCommonDataModel import vtkBezierQuadrilateral
218                    cell = vtkBezierQuadrilateral()
219                elif ct == vtki.cell_types["BEZIER_TETRAHEDRON"]:
220                    from vtkmodules.vtkCommonDataModel import vtkBezierTetra
221                    cell = vtkBezierTetra()
222                elif ct == vtki.cell_types["BEZIER_HEXAHEDRON"]:
223                    from vtkmodules.vtkCommonDataModel import vtkBezierHexahedron
224                    cell = vtkBezierHexahedron()
225                elif ct == vtki.cell_types["BEZIER_WEDGE"]:
226                    from vtkmodules.vtkCommonDataModel import vtkBezierWedge
227                    cell = vtkBezierWedge()
228                else:
229                    vedo.logger.error(
230                        f"UnstructuredGrid: cell type {ct} not supported. Skip.")
231                    continue
232
233                cpids = cell.GetPointIds()
234                cell_conn = cells[i]
235                for j, pid in enumerate(cell_conn):
236                    cpids.SetId(j, pid)
237                self.dataset.InsertNextCell(ct, cpids)
238
239        elif "UnstructuredGrid" in inputtype:
240            self.dataset = inputobj
241
242        elif isinstance(inputobj, str):
243            if "https://" in inputobj:
244                inputobj = download(inputobj, verbose=False)
245            self.filename = inputobj
246            if inputobj.endswith(".vtu"):
247                reader = vtki.new("XMLUnstructuredGridReader")
248            else:
249                reader = vtki.new("UnstructuredGridReader")
250            self.filename = inputobj
251            reader.SetFileName(inputobj)
252            reader.Update()
253            self.dataset = reader.GetOutput()
254
255        else:
256            # this converts other types of vtk objects to UnstructuredGrid
257            apf = vtki.new("AppendFilter")
258            try:
259                apf.AddInputData(inputobj)
260            except TypeError:
261                apf.AddInputData(inputobj.dataset)
262            apf.Update()
263            self.dataset = apf.GetOutput()
264
265        self.properties.SetColor(0.89, 0.455, 0.671)  # pink7
266
267        self.pipeline = utils.OperationNode(
268            self, comment=f"#cells {self.dataset.GetNumberOfCells()}", c="#4cc9f0"
269        )

Support for UnstructuredGrid objects.

Arguments:
  • inputobj : (list, vtkUnstructuredGrid, str) A list in the form [points, cells, celltypes], or a vtkUnstructuredGrid object, or a filename

Celltypes are identified by the following convention.

actor
412    @property
413    def actor(self):
414        """Return the `vtkActor` of the object."""
415        # print("building actor")
416        gf = vtki.new("GeometryFilter")
417        gf.SetInputData(self.dataset)
418        gf.Update()
419        out = gf.GetOutput()
420        self.mapper.SetInputData(out)
421        self.mapper.Modified()
422        return self._actor

Return the vtkActor of the object.

def merge(self, *others) -> Self:
435    def merge(self, *others) -> Self:
436        """
437        Merge multiple datasets into one single `UnstrcturedGrid`.
438        """
439        apf = vtki.new("AppendFilter")
440        for o in others:
441            if isinstance(o, UnstructuredGrid):
442                apf.AddInputData(o.dataset)
443            elif isinstance(o, vtki.vtkUnstructuredGrid):
444                apf.AddInputData(o)
445            else:
446                vedo.printc("Error: cannot merge type", type(o), c="r")
447        apf.Update()
448        self._update(apf.GetOutput())
449        self.pipeline = utils.OperationNode(
450            "merge", parents=[self, *others], c="#9e2a2b"
451        )
452        return self

Merge multiple datasets into one single UnstrcturedGrid.

def copy(self, deep=True) -> UnstructuredGrid:
454    def copy(self, deep=True) -> "UnstructuredGrid":
455        """Return a copy of the object. Alias of `clone()`."""
456        return self.clone(deep=deep)

Return a copy of the object. Alias of clone().

def clone(self, deep=True) -> UnstructuredGrid:
458    def clone(self, deep=True) -> "UnstructuredGrid":
459        """Clone the UnstructuredGrid object to yield an exact copy."""
460        ug = vtki.vtkUnstructuredGrid()
461        if deep:
462            ug.DeepCopy(self.dataset)
463        else:
464            ug.ShallowCopy(self.dataset)
465        if isinstance(self, vedo.UnstructuredGrid):
466            cloned = vedo.UnstructuredGrid(ug)
467        else:
468            cloned = vedo.TetMesh(ug)
469
470        cloned.copy_properties_from(self)
471
472        cloned.pipeline = utils.OperationNode(
473            "clone", parents=[self], shape="diamond", c="#bbe1ed"
474        )
475        return cloned

Clone the UnstructuredGrid object to yield an exact copy.

def bounds(self) -> numpy.ndarray:
477    def bounds(self) -> np.ndarray:
478        """
479        Get the object bounds.
480        Returns a list in format `[xmin,xmax, ymin,ymax, zmin,zmax]`.
481        """
482        # OVERRIDE CommonAlgorithms.bounds() which is too slow
483        return np.array(self.dataset.GetBounds())

Get the object bounds. Returns a list in format [xmin,xmax, ymin,ymax, zmin,zmax].

def threshold(self, name=None, above=None, below=None, on='cells') -> Self:
485    def threshold(self, name=None, above=None, below=None, on="cells") -> Self:
486        """
487        Threshold the tetrahedral mesh by a cell scalar value.
488        Reduce to only tets which satisfy the threshold limits.
489
490        - if `above = below` will only select tets with that specific value.
491        - if `above > below` selection range is flipped.
492
493        Set keyword "on" to either "cells" or "points".
494        """
495        th = vtki.new("Threshold")
496        th.SetInputData(self.dataset)
497
498        if name is None:
499            if self.celldata.keys():
500                name = self.celldata.keys()[0]
501                th.SetInputArrayToProcess(0, 0, 0, 1, name)
502            elif self.pointdata.keys():
503                name = self.pointdata.keys()[0]
504                th.SetInputArrayToProcess(0, 0, 0, 0, name)
505            if name is None:
506                vedo.logger.warning("cannot find active array. Skip.")
507                return self
508        else:
509            if on.startswith("c"):
510                th.SetInputArrayToProcess(0, 0, 0, 1, name)
511            else:
512                th.SetInputArrayToProcess(0, 0, 0, 0, name)
513
514        if above is not None:
515            th.SetLowerThreshold(above)
516
517        if below is not None:
518            th.SetUpperThreshold(below)
519
520        th.Update()
521        return self._update(th.GetOutput())

Threshold the tetrahedral mesh by a cell scalar value. Reduce to only tets which satisfy the threshold limits.

  • if above = below will only select tets with that specific value.
  • if above > below selection range is flipped.

Set keyword "on" to either "cells" or "points".

def isosurface(self, value=None, flying_edges=False) -> vedo.mesh.Mesh:
523    def isosurface(self, value=None, flying_edges=False) -> "vedo.mesh.Mesh":
524        """
525        Return an `Mesh` isosurface extracted from the `Volume` object.
526
527        Set `value` as single float or list of values to draw the isosurface(s).
528        Use flying_edges for faster results (but sometimes can interfere with `smooth()`).
529
530        Examples:
531            - [isosurfaces1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/isosurfaces1.py)
532
533                ![](https://vedo.embl.es/images/volumetric/isosurfaces.png)
534        """
535        scrange = self.dataset.GetScalarRange()
536
537        if flying_edges:
538            cf = vtki.new("FlyingEdges3D")
539            cf.InterpolateAttributesOn()
540        else:
541            cf = vtki.new("ContourFilter")
542            cf.UseScalarTreeOn()
543
544        cf.SetInputData(self.dataset)
545        cf.ComputeNormalsOn()
546
547        if utils.is_sequence(value):
548            cf.SetNumberOfContours(len(value))
549            for i, t in enumerate(value):
550                cf.SetValue(i, t)
551        else:
552            if value is None:
553                value = (2 * scrange[0] + scrange[1]) / 3.0
554                # print("automatic isosurface value =", value)
555            cf.SetValue(0, value)
556
557        cf.Update()
558        poly = cf.GetOutput()
559
560        out = vedo.mesh.Mesh(poly, c=None).flat()
561        out.mapper.SetScalarRange(scrange[0], scrange[1])
562
563        out.pipeline = utils.OperationNode(
564            "isosurface",
565            parents=[self],
566            comment=f"#pts {out.dataset.GetNumberOfPoints()}",
567            c="#4cc9f0:#e9c46a",
568        )
569        return out

Return an Mesh isosurface extracted from the Volume object.

Set value as single float or list of values to draw the isosurface(s). Use flying_edges for faster results (but sometimes can interfere with smooth()).

Examples:
def shrink(self, fraction=0.8) -> Self:
571    def shrink(self, fraction=0.8) -> Self:
572        """
573        Shrink the individual cells.
574
575        ![](https://vedo.embl.es/images/feats/shrink_hex.png)
576        """
577        sf = vtki.new("ShrinkFilter")
578        sf.SetInputData(self.dataset)
579        sf.SetShrinkFactor(fraction)
580        sf.Update()
581        out = sf.GetOutput()
582        self._update(out)
583        self.pipeline = utils.OperationNode(
584            "shrink", comment=f"by {fraction}", parents=[self], c="#9e2a2b"
585        )
586        return self

Shrink the individual cells.

def tomesh(self, fill=False, shrink=1.0) -> vedo.mesh.Mesh:
588    def tomesh(self, fill=False, shrink=1.0) -> "vedo.mesh.Mesh":
589        """
590        Build a polygonal `Mesh` from the current object.
591
592        If `fill=True`, the interior faces of all the cells are created.
593        (setting a `shrink` value slightly smaller than the default 1.0
594        can avoid flickering due to internal adjacent faces).
595
596        If `fill=False`, only the boundary faces will be generated.
597        """
598        gf = vtki.new("GeometryFilter")
599        if fill:
600            sf = vtki.new("ShrinkFilter")
601            sf.SetInputData(self.dataset)
602            sf.SetShrinkFactor(shrink)
603            sf.Update()
604            gf.SetInputData(sf.GetOutput())
605            gf.Update()
606            poly = gf.GetOutput()
607        else:
608            gf.SetInputData(self.dataset)
609            gf.Update()
610            poly = gf.GetOutput()
611
612        msh = vedo.mesh.Mesh(poly)
613        msh.copy_properties_from(self)
614        msh.pipeline = utils.OperationNode(
615            "tomesh", parents=[self], comment=f"fill={fill}", c="#9e2a2b:#e9c46a"
616        )
617        return msh

Build a polygonal Mesh from the current object.

If fill=True, the interior faces of all the cells are created. (setting a shrink value slightly smaller than the default 1.0 can avoid flickering due to internal adjacent faces).

If fill=False, only the boundary faces will be generated.

cell_types_array
619    @property
620    def cell_types_array(self):
621        """Return the list of cell types in the dataset."""
622        uarr = self.dataset.GetCellTypesArray()
623        return utils.vtk2numpy(uarr)

Return the list of cell types in the dataset.

def extract_cells_by_type(self, ctype) -> UnstructuredGrid:
625    def extract_cells_by_type(self, ctype) -> "UnstructuredGrid":
626        """Extract a specific cell type and return a new `UnstructuredGrid`."""
627        if isinstance(ctype, str):
628            try:
629                ctype = vtki.cell_types[ctype.upper()]
630            except KeyError:
631                vedo.logger.error(f"extract_cells_by_type: cell type {ctype} does not exist. Skip.")
632                return self
633        uarr = self.dataset.GetCellTypesArray()
634        ctarrtyp = np.where(utils.vtk2numpy(uarr) == ctype)[0]
635        uarrtyp = utils.numpy2vtk(ctarrtyp, deep=False, dtype="id")
636        selection_node = vtki.new("SelectionNode")
637        selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
638        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
639        selection_node.SetSelectionList(uarrtyp)
640        selection = vtki.new("Selection")
641        selection.AddNode(selection_node)
642        es = vtki.new("ExtractSelection")
643        es.SetInputData(0, self.dataset)
644        es.SetInputData(1, selection)
645        es.Update()
646
647        ug = UnstructuredGrid(es.GetOutput())
648        ug.pipeline = utils.OperationNode(
649            "extract_cell_type", comment=f"type {ctype}", c="#edabab", parents=[self]
650        )
651        return ug

Extract a specific cell type and return a new UnstructuredGrid.

def extract_cells_by_id(self, idlist, use_point_ids=False) -> UnstructuredGrid:
653    def extract_cells_by_id(self, idlist, use_point_ids=False) -> "UnstructuredGrid":
654        """Return a new `UnstructuredGrid` composed of the specified subset of indices."""
655        selection_node = vtki.new("SelectionNode")
656        if use_point_ids:
657            selection_node.SetFieldType(vtki.get_class("SelectionNode").POINT)
658            contcells = vtki.get_class("SelectionNode").CONTAINING_CELLS()
659            selection_node.GetProperties().Set(contcells, 1)
660        else:
661            selection_node.SetFieldType(vtki.get_class("SelectionNode").CELL)
662        selection_node.SetContentType(vtki.get_class("SelectionNode").INDICES)
663        vidlist = utils.numpy2vtk(idlist, dtype="id")
664        selection_node.SetSelectionList(vidlist)
665        selection = vtki.new("Selection")
666        selection.AddNode(selection_node)
667        es = vtki.new("ExtractSelection")
668        es.SetInputData(0, self)
669        es.SetInputData(1, selection)
670        es.Update()
671
672        ug = UnstructuredGrid(es.GetOutput())
673        pr = vtki.vtkProperty()
674        pr.DeepCopy(self.properties)
675        ug.actor.SetProperty(pr)
676        ug.properties = pr
677
678        ug.mapper.SetLookupTable(utils.ctf2lut(self))
679        ug.pipeline = utils.OperationNode(
680            "extract_cells_by_id",
681            parents=[self],
682            comment=f"#cells {self.dataset.GetNumberOfCells()}",
683            c="#9e2a2b",
684        )
685        return ug

Return a new UnstructuredGrid composed of the specified subset of indices.

def find_cell(self, p: list) -> int:
687    def find_cell(self, p: list) -> int:
688        """Locate the cell that contains a point and return the cell ID."""
689        if self.cell_locator is None:
690            self.cell_locator = vtki.new("CellLocator")
691            self.cell_locator.SetDataSet(self.dataset)
692            self.cell_locator.BuildLocator()
693        cid = self.cell_locator.FindCell(p)
694        return cid

Locate the cell that contains a point and return the cell ID.

def clean(self) -> Self:
696    def clean(self) -> Self:
697        """
698<